<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maxi Ferreira</title>
    <description>The latest articles on DEV Community by Maxi Ferreira (@charca).</description>
    <link>https://dev.to/charca</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F387459%2Fd2104b96-4012-478b-ba75-a184d8c22b92.png</url>
      <title>DEV Community: Maxi Ferreira</title>
      <link>https://dev.to/charca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/charca"/>
    <language>en</language>
    <item>
      <title>Password Protection for Cloudflare Pages</title>
      <dc:creator>Maxi Ferreira</dc:creator>
      <pubDate>Wed, 12 Jan 2022 19:46:18 +0000</pubDate>
      <link>https://dev.to/charca/password-protection-for-cloudflare-pages-8ma</link>
      <guid>https://dev.to/charca/password-protection-for-cloudflare-pages-8ma</guid>
      <description>&lt;p&gt;&lt;a href="https://pages.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Pages&lt;/a&gt; is a fantastic service for hosting static sites: it is extremely easy to set-up, it deploys your sites automatically on every commit to your GitHub or GitLab repos, and its free plan is incredibly generous; with unlimited users, sites, requests, and bandwidth.&lt;/p&gt;

&lt;p&gt;For the purposes of deploying and previewing static sites, Pages is very similar to products like Vercel or Netlify. However, one of the features it lacks in comparison to its main competitors is the ability to protect environments using a simple password-only authorization.&lt;/p&gt;

&lt;p&gt;You have the option to limit access to your Pages environment by integrating with Cloudflare's &lt;a href="https://www.cloudflare.com/teams/access/" rel="noopener noreferrer"&gt;Access&lt;/a&gt; product (which is free for up to 50 users), and you should definitely look into it if you're looking for a full-blown authentication mechanism.&lt;/p&gt;

&lt;p&gt;But if what you need is a basic layer of protection so that your sites are not immediately available to the public, a simple password-only authentication feature like the one offered by Netlify and Vercel might be exactly what you need.&lt;/p&gt;

&lt;p&gt;In this post I'm going to talk about how you can password-protect your Cloudflare Pages site by building a small authentication server powered by Cloudflare Workers; Cloudflare's serverless platform.&lt;/p&gt;

&lt;p&gt;You can see a demo of the final result here: &lt;a href="https://cloudflare-pages-auth.pages.dev/" rel="noopener noreferrer"&gt;https://cloudflare-pages-auth.pages.dev/&lt;/a&gt; (password: &lt;code&gt;password&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz8ovoimkhvn0dsfwvtm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz8ovoimkhvn0dsfwvtm.png" alt="Image description" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;If you want to add password-protection to your own Cloudflare Pages site, just head to the &lt;a href="https://github.com/Charca/cloudflare-pages-auth" rel="noopener noreferrer"&gt;repo&lt;/a&gt; and follow the instructions there.&lt;/p&gt;

&lt;p&gt;You basically need to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the contents of the &lt;code&gt;functions&lt;/code&gt; directory from the repo into your own project.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;CFP_PASSWORD&lt;/code&gt; environment variable to your Cloudflare Pages dashboard with the password you want to use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that's it! The next time you deploy, your site will be password-protected 🎉&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Note: You might want to update your Cloudflare project settings to be "&lt;a href="https://developers.cloudflare.com/pages/functions/pricing/#fail-closed" rel="noopener noreferrer"&gt;Failed closed&lt;/a&gt;" as well. Otherwise, your site will be unprotected if you reach your daily limit of Function requests. Thanks Thomas for letting me know about this!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're interested in learning more about how this works, just read along!&lt;/p&gt;




&lt;h2&gt;
  
  
  Pages, Workers, and Functions
&lt;/h2&gt;

&lt;p&gt;Cloudflare Pages is primarily a service for hosting static sites, which means that to run our small authentication application, we'll need a backend environment that can execute our server-side functions.&lt;/p&gt;

&lt;p&gt;That's where &lt;a href="https://workers.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt; come in, which is a serverless execution environment (similar to AWS Lambda or Vercel Edge Functions) that we can use to run our authentication application on Cloudflare's amazingly fast edge network.&lt;/p&gt;

&lt;p&gt;Pages and Workers are two separate products, and while they integrate really well together, if you want to build an application that uses them both, you'd typically need to create two separate projects and manage and deploy them individually. Thankfully, we can use a feature called Cloudflare Functions to make things a lot easier.&lt;/p&gt;

&lt;p&gt;Functions are a feature of Cloudflare Pages that serve as a link between our Pages site and a Workers environment. The advantage of using Functions is that we can manage and deploy them as part of our Pages project rather than having to create a separate Workers application.&lt;/p&gt;

&lt;p&gt;To create a function, we simply need to create a &lt;code&gt;functions&lt;/code&gt; folder in the root of our project, and add JavaScript or TypeScript files in there to handle the function's logic. This will also generate a routing table based on the file structure of this folder. So if we create the following script as &lt;code&gt;functions/api/hello-world.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/api/hello-world.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, world!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we deploy our site, this function will be available under the URL: &lt;code&gt;https://your-site.pages.dev/api/hello-world&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to learn more about Functions and Workers, check out the various resources on the &lt;a href="https://developers.cloudflare.com/pages/platform/functions" rel="noopener noreferrer"&gt;Cloudflare Docs&lt;/a&gt; site.&lt;/p&gt;




&lt;h2&gt;
  
  
  Middleware
&lt;/h2&gt;

&lt;p&gt;Our small authentication application needs a way to intercept all requests to our Pages project so that we can verify that the user has access to the site, or redirect them to the login page if they don't. We can do this using Middleware, which are a special type of function that sits between the user's request and the route handler.&lt;/p&gt;

&lt;p&gt;To create a middleware for all of the pages on our site, we need to add a &lt;code&gt;_middleware.js&lt;/code&gt; file to the &lt;code&gt;functions&lt;/code&gt; folder. Here's an example middleware that gives you a different response if you're trying to access the &lt;code&gt;/admin&lt;/code&gt; route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You need to log in!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A Simple Password-Protection Server
&lt;/h2&gt;

&lt;p&gt;Now that we've seen how Functions, Workers, and Middleware work, we can start designing our application so that it works on &lt;em&gt;any&lt;/em&gt; Pages site. We'll keep the application fairly simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We'll use a middleware to intercept all request to the site and redirect them to a login page if they're not authenticated.&lt;/li&gt;
&lt;li&gt;We'll create a route that handles submissions to the login form, and verifies that the user has provided the right password (which is stored in an environment variable).&lt;/li&gt;
&lt;li&gt;If they provide the right password, we'll set a cookie with a hash that subsequent requests will use to verify that they're authenticated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what the overall design looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fypcda5zsaw05ur5k98v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fypcda5zsaw05ur5k98v1.png" alt="Image description" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the complete implementation that powers this password-protection server in the &lt;code&gt;functions&lt;/code&gt; folder of the &lt;a href="https://github.com/Charca/cloudflare-pages-auth/tree/main/functions" rel="noopener noreferrer"&gt;example-repo&lt;/a&gt;. The folder contains 5 files (written in TypeScript, but you can remove the types and rename to &lt;code&gt;.js&lt;/code&gt; if you feel more comfortable with plain JavaScript):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_middleware.ts&lt;/code&gt; -&amp;gt; the middleware that intercepts all requests to our Pages site.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cfp_login.ts&lt;/code&gt; -&amp;gt; the function that handles POST request to the &lt;code&gt;/cfp_login&lt;/code&gt; route.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constants.ts&lt;/code&gt; -&amp;gt; a few constants you can use to customize the service to your liking.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;template.ts&lt;/code&gt; -&amp;gt; the HTML template for the login page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;utils.ts&lt;/code&gt; -&amp;gt; a couple of utility functions for encrypting passwords and working with cookies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is nothing too interesting going on in the &lt;code&gt;constants.ts&lt;/code&gt;, &lt;code&gt;template.ts&lt;/code&gt; and &lt;code&gt;utils.ts&lt;/code&gt; files, so I'm going to focus on the other two:&lt;/p&gt;

&lt;h3&gt;
  
  
  _middleware.ts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/_middleware.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CFP_ALLOWED_PATHS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getCookieKeyValue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getTemplate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CFP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieKeyValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCookieKeyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CFP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookieKeyValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;CFP_ALLOWED_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CFP_PASSWORD&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Correct hash in cookie, allowed path, or no password set.&lt;/span&gt;
    &lt;span class="c1"&gt;// Continue to next middleware.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No cookie or incorrect hash in cookie. Redirect to login.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;withError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we talked about before, this function intercepts all requests to our Pages site. If you look at the body of the function, it's nothing more than a big if/else statement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the request includes a cookie with the correct authentication hash, or if the path is on the list of allowed paths (paths that you &lt;em&gt;don't&lt;/em&gt; want to password-protect), or if the &lt;code&gt;CFP_PASSWORD&lt;/code&gt; environment variable is not set, continue to the next middleware, which in our case means respond with the route we were intercepting.&lt;/li&gt;
&lt;li&gt;Otherwise, respond with the contents of the &lt;code&gt;getTemplate()&lt;/code&gt; function, which is the HTML template of the login page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  cfp_login.ts
&lt;/h3&gt;

&lt;p&gt;The other interesting component of the application is the &lt;code&gt;cfp_login.ts&lt;/code&gt; function, which is yet another big if/else block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/cfp_login.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CFP_COOKIE_MAX_AGE&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getCookieKeyValue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRequestPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CFP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashedPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashedCfpPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CFP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashedPassword&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;hashedCfpPassword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Valid password. Redirect to home page and set cookie with auth hash.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieKeyValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCookieKeyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CFP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Set-Cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cookieKeyValue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; Max-Age=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CFP_COOKIE_MAX_AGE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; Path=/; HttpOnly; Secure`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Invalid password. Redirect to login page with error.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/?error=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we're exporting a function called &lt;code&gt;onRequestPost&lt;/code&gt; as opposed to the &lt;code&gt;onRequest&lt;/code&gt; function of the previous file. This is because we want this route to react to POST requests to the &lt;code&gt;/cfp_login&lt;/code&gt; path.&lt;/p&gt;

&lt;p&gt;The body of the function compares the hash of the password provided by the user via the login form with the hash of the password in the &lt;code&gt;CFP_PASSWORD&lt;/code&gt; environment variable. If they match, they've entered the right password, so we redirect them to the home page while also setting a cookie with the password's hash as the value.&lt;/p&gt;

&lt;p&gt;Otherwise, we'll redirect to the home page with the &lt;code&gt;?error=1&lt;/code&gt; query param set, which in our template we use to show an error message.&lt;/p&gt;

&lt;p&gt;The cookie we set has an expiration time of one week by default (which can be customized in the &lt;code&gt;constants.ts&lt;/code&gt; file). The cookie will be included on every subsequent request to our site, and as long as it has the correct value, it will pass the condition on the &lt;code&gt;_middleware.ts&lt;/code&gt; function, which will serve the request page directly without asking for the password again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting the Password
&lt;/h2&gt;

&lt;p&gt;The last thing we need to do is create the &lt;code&gt;CFP_PASSWORD&lt;/code&gt; environment variable with the password we want to use to protect our site. You can do this on your Page's site Dashboard under &lt;strong&gt;Settings -&amp;gt; Environment Variables&lt;/strong&gt;. You can set a different password for the Production and Preview environments if you want to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foiyew0oqbblzo07maj9q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foiyew0oqbblzo07maj9q.png" alt="Image description" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing the Password
&lt;/h3&gt;

&lt;p&gt;Our simple authentication server doesn't have actual "sessions", so there's nothing to invalidate if you decide to change the &lt;code&gt;CFP_PASSWORD&lt;/code&gt; environment variable with a different password.&lt;/p&gt;

&lt;p&gt;Changing the password will cause the hash from the cookie to no longer match the hash on the server, which will in turn prompt the user for the new password the next time they try to access a page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Locally
&lt;/h2&gt;

&lt;p&gt;To run your functions locally and test the password-protection on your own computer, you can use the &lt;a href="https://github.com/cloudflare/wrangler2" rel="noopener noreferrer"&gt;wrangler&lt;/a&gt; CLI using &lt;code&gt;npx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler pages dev build &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nv"&gt;CFP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that you'll need to pass the &lt;code&gt;CFP_PASSWORD&lt;/code&gt; environment variable when running the CLI command. If you don't pass it, the site will be served but it will not be password-protected.&lt;/p&gt;




&lt;p&gt;And that's all I've got!&lt;/p&gt;

&lt;p&gt;I hope you find this article and the example project useful. If you give it a try on your own Pages site, please let me know how it goes in the comments!&lt;/p&gt;

&lt;p&gt;Thank you for reading~ &amp;lt;3&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>serverless</category>
      <category>static</category>
      <category>deploy</category>
    </item>
    <item>
      <title>Getting Started with Lighthouse User Flows</title>
      <dc:creator>Maxi Ferreira</dc:creator>
      <pubDate>Fri, 19 Nov 2021 22:37:52 +0000</pubDate>
      <link>https://dev.to/charca/getting-started-with-lighthouse-user-flows-24dm</link>
      <guid>https://dev.to/charca/getting-started-with-lighthouse-user-flows-24dm</guid>
      <description>&lt;p&gt;The Google Chrome Team recently announced two big features coming to &lt;a href="https://developer.chrome.com/blog/lighthouse-9-0/" rel="noopener noreferrer"&gt;Lighthouse 9.0&lt;/a&gt; and Chrome DevTools: User Flow Reports in Lighthouse, and a new Recorder panel in DevTools that can capture and replay user journeys with just a few clicks.&lt;/p&gt;

&lt;p&gt;I've been experimenting with both of these tools for the past couple of weeks, and I've been genuinely impressed by how powerful they are and the possibilities they bring when you use them together.&lt;/p&gt;

&lt;p&gt;In this post, I want to share a quick overview of what these features are and how they work, and finally walk you through an example that combines them to unlock their full potential. Let's dive in!&lt;/p&gt;




&lt;h2&gt;
  
  
  Lighthouse User Flow Reports
&lt;/h2&gt;

&lt;p&gt;Unlike traditional Lighthouse reports (which only audit a website during its initial page-load), user flow reports can analyze a page at any point during its life-cycle. We can take "snapshots" of a page at a particular moment, or even collect metrics over a period of time that includes user interactions.&lt;/p&gt;

&lt;p&gt;User flows are available as a new API in the &lt;a href="https://github.com/GoogleChrome/lighthouse" rel="noopener noreferrer"&gt;Lighthouse Node module&lt;/a&gt;, and we can use them alongside tools like Puppeteer which allow us to control the browser and trigger synthetic events programmatically.&lt;/p&gt;

&lt;p&gt;Here's an example of how we can generate a user flow report using Puppeteer and the Lighthouse API (you can see the complete code &lt;a href="https://github.com/Charca/lighthouse-user-flows/blob/main/examples/navigation-report.js" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;captureReport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Puppeteer initialization&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Start user flow&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lighthouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My User Flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// ... Caputure reports here ...&lt;/span&gt;

  &lt;span class="c1"&gt;// End user flow&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateReport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within a user flow, there are three types of reports that we can capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Navigations&lt;/strong&gt; – to audit cold and warm page-loads,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots&lt;/strong&gt; – to audit the exact state of the page at any point in time, and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timespans&lt;/strong&gt; – to audit a page during any period of time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're going to see concrete examples of each one of them in the last section, but here's a quick overview of what they do and how their APIs look like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ukft25nj3v3e508i3s8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ukft25nj3v3e508i3s8.png" alt="Navigation Report Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigations
&lt;/h3&gt;

&lt;p&gt;These are the standard Lighthouse reports that audit a page during page-load, except that now we can measure both cold page-loads (clearing caches and local storage), and warm page-loads (without clearing the cache). We can even capture multiple navigation reports as part of the same user flow report see how they compare.&lt;/p&gt;

&lt;p&gt;This is how we can capture a navigation report with this new API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.nytimes.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also give the report a descriptive name with the &lt;code&gt;stepName&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.nytimes.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cold navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we can capture a warm load by setting the &lt;code&gt;disableStorageReset&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.nytimes.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Warm navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;configContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;settingsOverrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disableStorageReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonfwjd27wm5x8t2lg2z8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonfwjd27wm5x8t2lg2z8.png" alt="Snapshot Report Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshots
&lt;/h3&gt;

&lt;p&gt;We can take a snapshot at any point during the user flow, and Lighthouse will analyze the page in its exact state. This is useful for when we want to audit a particular state of the UI that only appears after a user interaction – like a modal that shows up when the user clicks a button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout modal opened&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we're only analyzing a single moment and not a period of time, the metrics in the snapshot report are not terribly useful for performance, but they're a great way to get accessibility and best practices insights based on the state of the page after the user interacts with it, which is something that wasn't possible before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1k51r2k1e7oje0v1jjjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1k51r2k1e7oje0v1jjjq.png" alt="Timespan Report Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestamps
&lt;/h3&gt;

&lt;p&gt;These reports audit a website over a period of time, which can contain user interactions as well. From a performance perspective, they're useful to measure Total Blocking Time (TBT) and Cumulative Layout Shift (CLS) while a user interacts with a page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTimespan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ... user interactions here ...&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endTimespan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measuring CLS beyond the initial page is particularly useful because it gives us a more accurate measure of this metric in the lab, that is closer to what we'll see in our field data.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;For a much more detailed overview of Lighthouse user flows with complete code examples, I highly recommend checking out the &lt;a href="https://web.dev/lighthouse-user-flows/" rel="noopener noreferrer"&gt;official tutorial&lt;/a&gt; on web.dev.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7gihdl0e2do53m4ud57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7gihdl0e2do53m4ud57.png" alt="User Flow Reports Explained"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Chrome DevTools Recorder Panel
&lt;/h2&gt;

&lt;p&gt;The Recorder panel is a new feature coming to Chrome DevTools (currently available in Chrome 97), which allow us to record and replay user journeys with just a few clicks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0xvc1wz603z02q2hvb7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0xvc1wz603z02q2hvb7.gif" alt="Chrome DevTools Recorder Panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the time of writing, the Recorder panel is only available in the &lt;a href="https://www.google.com/chrome/dev/" rel="noopener noreferrer"&gt;Chrome Dev&lt;/a&gt; and &lt;a href="https://www.google.com/chrome/canary/" rel="noopener noreferrer"&gt;Canary&lt;/a&gt; builds, so make sure you have one of those installed if you'd like to follow along.&lt;/p&gt;

&lt;p&gt;You can find the Recorder panel in DevTools under &lt;strong&gt;More options&lt;/strong&gt; &amp;gt; &lt;strong&gt;More tools&lt;/strong&gt; &amp;gt; &lt;strong&gt;Recorder&lt;/strong&gt;, or by opening the Command Menu (with &lt;strong&gt;Cmd + Shift + P&lt;/strong&gt;) and searching for &lt;strong&gt;Recorder&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwbs6m4uhr77mqs26d0p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwbs6m4uhr77mqs26d0p.png" alt="Recorder How To Find It"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the Recorder panel opened, you can click the &lt;strong&gt;Start new recording&lt;/strong&gt; button, give the recording a name, and start interacting with the page in any way you want (for example, completing an sign up or checkout flow). Once you're done with the recording, you'll be able to replay it, modify it, run a performance profile for the entire journey, or export the recording as a Puppeteer script.&lt;/p&gt;

&lt;p&gt;This last feature is what we're mostly interested in. We can use the auto-generated Puppeteer script as a starting point for creating user flow reports with Lighthouse, which will save us a ton of time and effort. We'll explore this approach next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lighthouse Reports on User Journeys
&lt;/h2&gt;

&lt;p&gt;Now that we've seen what Lighthouse user flow reports are and how we can record user journeys and export them as Puppeteer scripts with the DevTools Recorder panel, let's explore how we can use them together to capture a user flow report based on a user journey.&lt;/p&gt;

&lt;p&gt;The process is simple: we'll record a user journey in DevTools, export it as a Puppeteer script, and we'll modify the script by adding a few calls to the Lighthouse user flow APIs in the right places. In the end, we'll be able to run the script with Node.js and get shiny new User Flow Report back.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Project setup
&lt;/h3&gt;

&lt;p&gt;The first thing we need to do is initialize a new npm project (ideally in a new folder) and install the dependencies we're going to be using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir lighthouse-reports
cd lighthouse-reports
npm init -y
npm install lighthouse puppeteer open --save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll use the &lt;code&gt;open&lt;/code&gt; package so that we can automatically open the HTML report in the browser once the script finishes, but this is an optional dependency (and you definitely don't need it if your running the script in CI).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Record and export a user journey
&lt;/h3&gt;

&lt;p&gt;For this tutorial, I'm going to use this &lt;a href="https://coffee-cart.netlify.app/" rel="noopener noreferrer"&gt;coffee shopping&lt;/a&gt; demo app (borrowed from Google's documentation) to record a simple user journey: adding a couple of items to the cart, navigating to the shopping cart, and going through the (fake) checkout process.&lt;/p&gt;

&lt;p&gt;I encourage you to do something similar to keep things simple, but you can of course use any website you want and go wild with your user journey. Simply hit the "Start recording" button on the Recorder panel and start interacting with the page by clicking around, scrolling, or filling out forms. Make sure you stop the recording once you're done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpyfs790g3t99dkmrikl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpyfs790g3t99dkmrikl.gif" alt="Example Recording Animation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've finished recording, make sure you can replay it by hitting the &lt;strong&gt;Replay&lt;/strong&gt; button on the top right. &lt;strong&gt;This is important.&lt;/strong&gt; If the flow can't be replayed consistently, you might run into issues generating the Lighthouse reports later on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For more details on how to use the Recorder panel, check you &lt;a href="https://developer.chrome.com/docs/devtools/recorder/" rel="noopener noreferrer"&gt;this great article&lt;/a&gt; in the Chrome DevTools documentation site.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you're happy with your recording, export the user flow as a Puppeteer script by clicking the Export icon on the top (be careful not to click the delete icon by mistake, they're dangerously close together!), and save it in the project folder as &lt;code&gt;user-flow.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Edit the script
&lt;/h3&gt;

&lt;p&gt;Now comes the fun part. If you open the &lt;code&gt;user-flow.js&lt;/code&gt; script, you'll find that it consists of a bunch of utility functions at the top, followed by a series of code blocks, each one representing a "step" in our user journey (clicks, scrolls, keyboard events, etc.)&lt;/p&gt;

&lt;p&gt;We're going to make a few modifications to this script to generate a Lighthouse user flow report consisting of four "sub-reports":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two navigation reports (to measure both cold and warm page-loads),&lt;/li&gt;
&lt;li&gt;A snapshot report to capture the state of the page when the checkout modal is open, and&lt;/li&gt;
&lt;li&gt;A timespan report to capture the entire checkout flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might find it easier to see the modifications to the script in &lt;a href="https://github.com/Charca/lighthouse-user-flows/blob/main/examples/user-flow-modified.js" rel="noopener noreferrer"&gt;this annotated file&lt;/a&gt; or in &lt;a href="https://github.com/Charca/lighthouse-user-flows/compare/modify-script" rel="noopener noreferrer"&gt;this diff&lt;/a&gt;, but if you prefer a step-by-step guide, just read on and code along!&lt;/p&gt;

&lt;h4&gt;
  
  
  3.1 Import dependencies
&lt;/h4&gt;

&lt;p&gt;First off, let's import the rest of our dependencies right after the &lt;code&gt;puppeteer&lt;/code&gt; require in the first line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lighthouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lighthouse/lighthouse-core/fraggle-rock/api.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3.2 Create the user flow
&lt;/h4&gt;

&lt;p&gt;Right at the top of the script's main function, you'll find a couple of lines that create the Puppeteer &lt;code&gt;browser&lt;/code&gt; and &lt;code&gt;page&lt;/code&gt; instances. We'll create our user &lt;code&gt;flow&lt;/code&gt; instance right after that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lighthouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My User Flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3.3 Add the Navigation reports
&lt;/h4&gt;

&lt;p&gt;Now we need to scroll down to where the code blocks start. We'll add the two navigation reports right after the block with the &lt;code&gt;targetPage.goto('https://coffee-cart.netlify.app/')&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cold navigation report&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://coffee-cart.netlify.app/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cold navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Warm navigation report&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://coffee-cart.netlify.app/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Warm navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;configContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;settingsOverrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disableStorageReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3.4 Add the Snapshot report
&lt;/h4&gt;

&lt;p&gt;You can add this call between any two steps in the script, but for demonstration purposes, we want to take the snapshot once the Checkout modal opens. Add the following code right after the code block with the &lt;code&gt;waitForSelector&lt;/code&gt; call that waits for the &lt;code&gt;"aria/Proceed to checkout"&lt;/code&gt; element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout modal opened&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3.5 Add the Timespan report
&lt;/h4&gt;

&lt;p&gt;We'll start the timespan right after the &lt;code&gt;snapshot()&lt;/code&gt; call from the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTimespan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we'll end it at the end of the flow, right before the call to &lt;code&gt;browser.close()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endTimespan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3.6 Generate the user flow report
&lt;/h4&gt;

&lt;p&gt;Finally, we need to generate the report, save it as an HTML file, and open it in the browser. Add the following lines right before the end of the main function (after the call to &lt;code&gt;browser.close()&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reportPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/user-flow.report.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateReport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reportPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reportPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we're done! If you save and run the script (with &lt;code&gt;node user-flow.js&lt;/code&gt;), you should see the report coming up on your browser after a few moments. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkfz3wo97n8a3sunp7vl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkfz3wo97n8a3sunp7vl.png" alt="User Flow Report Screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you didn't follow the steps with me but would like to see how the report looks like, you can open the live report and play with it &lt;a href="https://lighthouse-user-flow-report.netlify.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. You'll see a timeline with our four reports in the order we captured them, and you can click in each one for a more detailed view. How cool is that?!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Lighthouse user flows and the new DevTools Recorder panel are like milk and cookies: they're both amazing on their own, but they're definitely better together.&lt;/p&gt;

&lt;p&gt;The new Lighthouse APIs enable new ways to measure the performance and accessibility of our websites, generating lab data that is more representative of what real users experience in the field. And with the auto-generated Puppeteer scripts, the process of capturing these data is a breeze.&lt;/p&gt;

&lt;p&gt;The Recorder panel also has many interesting use cases in addition to measuring performance. We can use the Puppeteer scripts as a starting point for running automated end-to-end tests, or even use them directly as a quick way to assert that user journeys can be completed correctly. And of course, since these are just Node scripts, we can run them as an additional step in our CI pipelines.&lt;/p&gt;

&lt;p&gt;Finally, I think it's important to keep in mind that these features are still quite young, so you may run into a few issues here and there (if you do, be sure to share your feedback with the Chrome team!). I still encourage you to give them a try and explore the cool things you can do with them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For more information about Lighthouse user flow reports, check out the &lt;a href="https://web.dev/lighthouse-user-flows/" rel="noopener noreferrer"&gt;official tutorial&lt;/a&gt; on web.dev.&lt;/li&gt;
&lt;li&gt;For detailed instructions about the DevTools Recorder panel, take a look at the &lt;a href="https://developer.chrome.com/docs/devtools/recorder/" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; on Chrome Developers.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I would love to hear your thoughts about this process if you give it a try! Please let me know in the comments, or reach out on &lt;a href="https://twitter.com/charca" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading ❤️&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>webperf</category>
    </item>
  </channel>
</rss>
