<?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>Forem: Stefano Magni</title>
    <description>The latest articles on Forem by Stefano Magni (@noriste).</description>
    <link>https://forem.com/noriste</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%2F195487%2F92699b7d-19a0-4209-8c82-c908c9ae4a7b.jpg</url>
      <title>Forem: Stefano Magni</title>
      <link>https://forem.com/noriste</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/noriste"/>
    <language>en</language>
    <item>
      <title>How Preply improved INP on a Next.js application (without React Server Components and App Router)</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Thu, 13 Feb 2025 09:54:29 +0000</pubDate>
      <link>https://forem.com/noriste/how-preply-improved-inp-on-a-nextjs-application-without-react-server-components-and-app-router-j8c</link>
      <guid>https://forem.com/noriste/how-preply-improved-inp-on-a-nextjs-application-without-react-server-components-and-app-router-j8c</guid>
      <description>&lt;p&gt;&lt;em&gt;This article has been originally posted &lt;a href="https://medium.com/preply-engineering/how-preply-improved-inp-on-a-next-js-application-without-react-server-components-and-app-router-491713149875" rel="noopener noreferrer"&gt;on Preply's engineering blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To improve our two most important pages from an SEO and SEM perspective, we started digging into how to improve their INP, a metric to assess web pages’ interaction speed. It was an &lt;strong&gt;R&amp;amp;D project driven by a clear goal&lt;/strong&gt; baked by tons of data and assumptions. We spent a lot of time identifying what to optimize and &lt;strong&gt;made one-line changes and big refactors&lt;/strong&gt;. We jumped onto React Server Components and Next.js App Router. We succeeded but also failed frequently. In this article, we will share the whole journey and the takeaways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why care about page speed?
&lt;/h3&gt;

&lt;p&gt;Page speed is crucial for web pages because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;From an SEO perspective, &lt;strong&gt;page speed is a ranking factor&lt;/strong&gt; for search engines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From an SEM perspective, page speed can influence your Quality Score on platforms like Google Ads. A higher Quality Score &lt;strong&gt;can lead to lower cost-per-click&lt;/strong&gt; (CPC) and better ad ranks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From a user perspective, a faster page speed can lead to better user engagement and higher conversion rates.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Preply.com is a B2C and B2B tutoring platform&lt;/strong&gt;, so SEO and SEM optimizations are part of our everyday job. Page speed impacts the effectiveness of our efforts in these two areas. Page speed is measured through &lt;a href="https://web.dev/articles/vitals" rel="noopener noreferrer"&gt;Web Vitals&lt;/a&gt;, which quantify a website’s user experience. One of the &lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals" rel="noopener noreferrer"&gt;Core Web Vitals&lt;/a&gt; is INP, the worst Web Vital of Preply.com.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is INP?
&lt;/h3&gt;

&lt;p&gt;The official definition of &lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;Interaction to Next Paint (INP)&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;INP is a metric that assesses a page’s overall responsiveness to user interactions by observing &lt;strong&gt;the latency of all click, tap, and keyboard interactions&lt;/strong&gt; that occur throughout the lifespan of a user’s visit to a page. The final &lt;strong&gt;INP value is the longest interaction observed&lt;/strong&gt;, ignoring outliers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Practically speaking, in a simplified way, and thinking of a “normal” MPA (Multi-Page application):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A user visits a web page and interacts with it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;INP measures, in milliseconds, all the synchronous JavaScript executed after the interaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The longest interaction is the user session’s INP for the web page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The overall page’s INP is the 75th percentile&lt;/strong&gt; of all the user sessions’ INP, split by desktop and mobile devices, over the last 28 days.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some key things to keep in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It doesn’t matter what the users do&lt;/strong&gt; on the web page. Some close the privacy banner, and some spend 15 minutes on a page doing many things. The slowest interaction is their INP.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Users don’t have the high-end devices we typically use to develop digital products. Regarding mobile devices, some users have very old and very slow smartphones (the number of these heavily depends on the markets you operate in). So your users could deal with some performance issues you were probably unaware of. &lt;strong&gt;The slower the device, the worse the INP&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks to the recent Google Chrome updates, working locally with INP is quite straightforward, look at the following screenshot of the devtools’ Performance tab, which includes INP.&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%2Fmlbq2vob9m0n2sor73t4.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%2Fmlbq2vob9m0n2sor73t4.png" alt="The Google Chrome devtools’s Performance tab shows the Core Web Vitals for the current session, especially INP, and the slowest interaction that caused it." width="800" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Google Chrome devtools’s Performance tab shows the Core Web Vitals for the current session, especially INP, and the slowest interaction that caused it.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Preply.com’s tech stack
&lt;/h3&gt;

&lt;p&gt;All Preply.com’s indexed pages are managed through a &lt;strong&gt;Multi-page Next.js 13 application (React 18) rendered on the server through the Pages router&lt;/strong&gt;. The codebase is quite big and has accumulated a lot of technical debt.&lt;/p&gt;

&lt;p&gt;The Next.js app has thousands of indexed pages, but the two most important ones, from either an SEO or SEM perspective, are &lt;strong&gt;the Home page and the Search page&lt;/strong&gt;. They are two very different pages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Home page has few lines of code and is mostly static. The only interactive elements are a slider and the privacy policy banner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Search page is huge, complex, and 90% interactive, and it includes modals to filter tutors, and book lessons.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Web Vitals on both pages are ok’ish, but &lt;strong&gt;there’s room for improvement on mobile devices’ INP&lt;/strong&gt;. The project’s goal is to improve INP from the yellow zone to the green one.&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%2F0dt1i69rx1hgc58cgorq.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%2F0dt1i69rx1hgc58cgorq.png" alt="Our internal dashboard reports the INP for the Home page (~250 ms before the optimizations, 185 ms after the optimizations) and the Search page (~250 ms before the optimizations, 175 ms after the optimizations)." width="800" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Our internal dashboard reports the INP for the Home page (~250 ms before the optimizations, 185 ms after the optimizations) and the Search page (~250 ms before the optimizations, 175 ms after the optimizations).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We calculated that moving the two pages INP in the green zone &lt;strong&gt;could save us $200K/year.&lt;/strong&gt; Please note that’s a ballpark number we identified by analyzing our current SEO situation, our competitor’s Web Vitals and estimating how much we can improve there.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our initial hypotheses
&lt;/h2&gt;

&lt;p&gt;We had two main hypotheses on how we could have improved INP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We need to &lt;strong&gt;reduce React’s hydration&lt;/strong&gt; to get the pages ready and interactive sooner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We need to &lt;strong&gt;improve the performance of our own JS/React code&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every change we introduced was the result of extensive analysis. Performance optimizations on existing big products usually require weeks of analysis and data intersection to find the root issues and only hours to fix them. This project is no exception: we started with many unknowns, and we moved through them, failure by failure, before finding out how to improve INP.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reducing React hydration
&lt;/h2&gt;

&lt;p&gt;Hydration is React’s process of making static HTML interactive on the client side. This is a common step for every SSR (Server-Side Rendered) website. Before hydration, the React app was not interactive. After hydration, it is fully interactive. But what’s in the middle?&lt;/p&gt;

&lt;p&gt;When users interact with the page, and hydration is in progress, React records and replays their interactions. This is very convenient from a developer perspective, but it comes with a cost: interactions’ INP is worse than normal. The next graph shows how a hypothetical 80-ms INP interaction becomes a 235-ms INP interaction &lt;strong&gt;if it happens during the hydration phase&lt;/strong&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%2Fd2cua3gq3ew760229hna.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%2Fd2cua3gq3ew760229hna.png" alt="The diagram shows how a usually fast interaction can become slow during hydration." width="800" height="581"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The diagram shows how a usually fast interaction can become slow during hydration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The above is an example. We can’t tell if the full hydration time is added to INP, but our manual tests showed that some delay is added to the normal INP.&lt;/p&gt;

&lt;p&gt;The most hyped recent React feature is &lt;a href="https://react.dev/reference/rsc/server-components" rel="noopener noreferrer"&gt;Server Components (RSC)&lt;/a&gt;. Thanks to RSC, you can selectively make part of your page static, &lt;strong&gt;executing it only on the server&lt;/strong&gt;, without the client-side hydration, similar to how good old PHP does. Next.js supports RSC through the &lt;a href="https://nextjs.org/docs/app" rel="noopener noreferrer"&gt;App Router&lt;/a&gt;, and migrating the two pages we worked on was the initial big bet.&lt;/p&gt;

&lt;p&gt;Let me anticipate one key conclusion: by looking at our metrics, we found out hydration &lt;strong&gt;should not be a big INP offender&lt;/strong&gt; since it’s happening quite fast (120 ms on average), and we expect most of the users to interact after this quick phase:&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%2Fe4c3px2nu06minsnbe22.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%2Fe4c3px2nu06minsnbe22.png" alt="At the time of writing, the INP for the Search page is 248 ms, and React hydration is 120 ms." width="800" height="312"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;At the time of writing, the INP for the Search page is 248 ms, and React hydration is 120 ms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Please note that our app leverages React 18, which introduced various Hydration improvements. We will touch this topic later in the article.&lt;/p&gt;

&lt;p&gt;Despite that, &lt;strong&gt;we migrated the Home page to the Next.js App Router anyway&lt;/strong&gt; to gain experience. Ultimately, one of the project’s most important goals was to collect all the possible takeaways to determine the best approach for all the other existing pages in the future.&lt;/p&gt;
&lt;h3&gt;
  
  
  All the data we leveraged to improve INP
&lt;/h3&gt;

&lt;p&gt;The “What do the users do on the web pages? What do they interact with?” questions are crucial to determining &lt;strong&gt;what to optimize&lt;/strong&gt;. To find out, we needed a ton of data. Preply is a very data-oriented company; we had all the data upfront. Without all this data, bringing INP to the green zone in less than a quarter would have been impossible.&lt;/p&gt;

&lt;p&gt;To improve INP, we used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The official &lt;a href="https://github.com/GoogleChrome/web-vitals" rel="noopener noreferrer"&gt;web-vitals&lt;/a&gt; library is from the Google Chrome team. We collect all the Web Vitals data at every session and create a global dashboard in DataDog to show the Web Vitals data.&lt;/li&gt;
&lt;/ul&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%2Fwrh25oxhb7tvsmp58ju6.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%2Fwrh25oxhb7tvsmp58ju6.png" alt="The generic DataDog dashboard we built on top of web-vitals library’s data." width="800" height="394"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The generic DataDog dashboard we built on top of web-vitals library’s data.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Next.js’ &lt;a href="https://nextjs.org/docs/pages/api-reference/functions/use-report-web-vitals#custom-metrics" rel="noopener noreferrer"&gt;custom metrics&lt;/a&gt; (especially the hydration and render duration).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All the mentioned data is available and queryable in SnowFlake, so everyone in Preply can analyze it and create custom dashboards.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.hotjar.com/" rel="noopener noreferrer"&gt;Hotjar&lt;/a&gt;, to show and analyze the heat maps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The standard browser dev tools for profiling. &lt;a href="https://react.dev/learn/react-developer-tools" rel="noopener noreferrer"&gt;React Developer Tools&lt;/a&gt; and &lt;a href="https://github.com/aidenybai/react-scan" rel="noopener noreferrer"&gt;React Scan&lt;/a&gt; to catch unnecessary re-renders.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For your information, we removed &lt;a href="https://www.datadoghq.com/product/real-user-monitoring/" rel="noopener noreferrer"&gt;DataDog’s RUM&lt;/a&gt; and &lt;a href="https://docs.sentry.io/product/insights/frontend/web-vitals/" rel="noopener noreferrer"&gt;Sentry Web Vitals&lt;/a&gt; because they were redundant compared to point 1 (the web-vitals library). We tried them out, but in the end, &lt;strong&gt;we all rely on our custom Snowflake instance&lt;/strong&gt; when it comes to querying and aggregating data (all data passes through Snowflake, and then we forward some data to DataDog), so it was a natural choice to keep everything centralized and remove redundancy.&lt;/p&gt;

&lt;p&gt;One impactful detail regarding the data the web-vitals library tracks: one of our most important dashboards showed a high number (75%) of &lt;code&gt;&amp;lt;unidentified&amp;gt;&lt;/code&gt; selectors. This defeats the purpose of the dashboard itself. The reason is &lt;a href="https://github.com/GoogleChrome/web-vitals/issues/567" rel="noopener noreferrer"&gt;a known Google Chrome issue&lt;/a&gt;, which makes it harder to collect the selector of the DOM elements when they are part of &lt;strong&gt;modals that disappear right after interactions&lt;/strong&gt;. The web-vitals authors suggested working around this issue in &lt;a href="https://github.com/GoogleChrome/web-vitals/issues/567#issuecomment-2539381555" rel="noopener noreferrer"&gt;this GitHub comment&lt;/a&gt;. Thanks to all the data we intersected, we were able to improve INP even without fixing the selector issue.&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%2Fl5cqeo87ucb04qyhkanv.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%2Fl5cqeo87ucb04qyhkanv.png" alt="The custom dashboard with the selectors of the INP offenders shows a very high number of unidentified elements." width="800" height="431"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The custom dashboard with the selectors of the INP offenders shows a very high number of unidentified elements.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How we improved INP
&lt;/h2&gt;

&lt;p&gt;The list of changes that improved INP on the &lt;strong&gt;Search page&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;INP before&lt;/th&gt;
&lt;th&gt;INP after&lt;/th&gt;
&lt;th&gt;Net improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upgrading React from 17 to 18&lt;/td&gt;
&lt;td&gt;~460 ms&lt;/td&gt;
&lt;td&gt;~320 ms&lt;/td&gt;
&lt;td&gt;-140 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtualizing a long list&lt;/td&gt;
&lt;td&gt;~320 ms&lt;/td&gt;
&lt;td&gt;~280 ms&lt;/td&gt;
&lt;td&gt;-40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debouncing keyboard events&lt;/td&gt;
&lt;td&gt;~280 ms&lt;/td&gt;
&lt;td&gt;~260 ms&lt;/td&gt;
&lt;td&gt;-20 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memoizing heavy components&lt;/td&gt;
&lt;td&gt;~260 ms&lt;/td&gt;
&lt;td&gt;~230 ms&lt;/td&gt;
&lt;td&gt;-30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixing state management non-EU privacy policy banner&lt;/td&gt;
&lt;td&gt;~230 ms&lt;/td&gt;
&lt;td&gt;~215 ms&lt;/td&gt;
&lt;td&gt;-15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avoiding the tooltip to re-render the whole page&lt;/td&gt;
&lt;td&gt;~215 ms&lt;/td&gt;
&lt;td&gt;~205 ms&lt;/td&gt;
&lt;td&gt;-10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updating Usercentrics for the EU privacy policy banner&lt;/td&gt;
&lt;td&gt;~205 ms&lt;/td&gt;
&lt;td&gt;~200 ms&lt;/td&gt;
&lt;td&gt;-5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-optimizing what we already optimized&lt;/td&gt;
&lt;td&gt;~200 ms&lt;/td&gt;
&lt;td&gt;~185 ms&lt;/td&gt;
&lt;td&gt;-15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The list of changes that improved INP on the &lt;strong&gt;Home page&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;INP before&lt;/th&gt;
&lt;th&gt;INP after&lt;/th&gt;
&lt;th&gt;Net improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rewriting the Slider and Improving the Accordion components&lt;/td&gt;
&lt;td&gt;~250 ms&lt;/td&gt;
&lt;td&gt;~220 ms&lt;/td&gt;
&lt;td&gt;-30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updating Usercentrics for the EU privacy policy banner&lt;/td&gt;
&lt;td&gt;~220 ms&lt;/td&gt;
&lt;td&gt;~180 ms&lt;/td&gt;
&lt;td&gt;-40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrating the Home page to the App router&lt;/td&gt;
&lt;td&gt;~180 ms&lt;/td&gt;
&lt;td&gt;~170 ms&lt;/td&gt;
&lt;td&gt;-10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We didn’t see any meaningful improvement by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Leveraging React’s &lt;code&gt;useDeferredValue&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimizing the Design System components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleaning up the Search page’s code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Quickly optimizing Preply Chat for the logged-in users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removing DataDog RUM.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, what we didn’t do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Offloading the third-party scripts with Partytown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Jumping on the new React Compiler.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find more details in the next sections.&lt;/p&gt;
&lt;h3&gt;
  
  
  React 18 upgrade (INP decreased by 140 ms)
&lt;/h3&gt;

&lt;p&gt;In reality, this was not part of the late 2024 page speed project this article is about, but of course, it’s worth mentioning. At the beginning of 2024, all the Product teams, coordinated by Preply’s Devex team, worked together to upgrade React to v18. This upgrade promised to bring meaningful hydration and performance improvements by itself, even without leveraging &lt;a href="https://react.dev/reference/react/Suspense" rel="noopener noreferrer"&gt;React Suspense&lt;/a&gt;, and so it was. In the days after deploying the version bump, we started seeing a much better INP.&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%2Fko68b6r8l7f8eavk5oli.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%2Fko68b6r8l7f8eavk5oli.png" alt="The INP of the Search page before the React 18 migration (~460 ms) dropped significantly immediately after the React 18 upgrade was released (to ~323 ms)." width="800" height="401"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The INP of the Search page before the React 18 migration (~460 ms) dropped significantly immediately after the React 18 upgrade was released (to ~323 ms).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The “&lt;a href="https://themobilereality.com/blog/hydration-ssr-with-react-18" rel="noopener noreferrer"&gt;Understanding Hydration in SSR with React 18’s New Architecture&lt;/a&gt;” article provides a longer explanation of React 18's hydration improvements.&lt;/p&gt;
&lt;h3&gt;
  
  
  Virtualizing a long list (INP decreased by 40 ms)
&lt;/h3&gt;

&lt;p&gt;The Search page’s longest list is the Tutor’s Country of Birth. It lists ~300 countries.&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%2Fcj498et7dvuf1h03rece.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%2Fcj498et7dvuf1h03rece.png" alt="The Country of Birth’s UI on Preply’s Search page." width="800" height="1523"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Country of Birth’s UI on Preply’s Search page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It doesn’t sound like a huge list. Still, the 300 React components and their sub-children take significant time to be added to the DOM on a slow smartphone (please note: we are speaking about the React-&amp;gt;DOM reconciliation that adds all the new elements to the DOM, not the render step itself). We used &lt;a href="https://virtuoso.dev/" rel="noopener noreferrer"&gt;React Virtuoso&lt;/a&gt; since it was already used in our front-end projects, but virtualization libraries are all quite similar.&lt;/p&gt;

&lt;p&gt;FYI: We discarded it because it’s not yet fully supported by the browser Preply.com supports, but &lt;a href="https://caniuse.com/css-content-visibility" rel="noopener noreferrer"&gt;CSS content-visibility&lt;/a&gt; has great potential to leverage CSS native virtualization. You can read more in the “&lt;a href="https://nolanlawson.com/2024/09/18/improving-rendering-performance-with-css-content-visibility/" rel="noopener noreferrer"&gt;Improving rendering performance with CSS content-visibility&lt;/a&gt;” article.&lt;/p&gt;
&lt;h3&gt;
  
  
  Debouncing keyboard events (INP decreased by 20 ms)
&lt;/h3&gt;

&lt;p&gt;Thanks to the data we collected through the web-vitals library, we split the keyboard events, which showed a constantly bad INP. There are three input fields on the Search page, and typing something there was painful on a slow device. As you can see in the next screenshot, debouncing them quickly brought noticeable INP improvements.&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%2Fd3i93zypxfh5wcgkkk13.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%2Fd3i93zypxfh5wcgkkk13.png" alt="Before: 632 ms INP while typing on the main learning subject’s input field, with most keyboard events resulting in a &amp;gt;300 ms INP. After, the same events result in a 72 ms INP." width="800" height="379"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before: 632 ms INP while typing on the main learning subject’s input field, with most keyboard events resulting in a &amp;gt;300 ms INP. After, the same events result in a 72 ms INP.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Memoizing heavy components (INP decreased by 30 ms)
&lt;/h3&gt;

&lt;p&gt;Two big components, namely the “How Preply works” and the “Users’ reviews” component, took a lot of time to re-render, increasing the INP of the interactions that resulted in whole page re-renders (for instance: when you close the Filters modal and the new filters are applied) You can see their impact in the next screenshot of the React DevTools’ profiler.&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%2Fqlsood72xh746j44l77b.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%2Fqlsood72xh746j44l77b.png" alt="The React DevTools’ profiler, in flamegraph view, shows the impact of the HowPreplyWorks and Reviews components." width="800" height="629"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The React DevTools’ profiler, in flamegraph view, shows the impact of the HowPreplyWorks and Reviews components.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Improving INP here was straightforward: the two components were static, and &lt;a href="https://react.dev/reference/react/memo" rel="noopener noreferrer"&gt;memoizing them&lt;/a&gt; was enough to improve INP.&lt;/p&gt;
&lt;h3&gt;
  
  
  Fixing state management non-EU privacy policy banner (INP decreased by 15 ms)
&lt;/h3&gt;

&lt;p&gt;The issue was, again, a “from global to local React state” problem. Every new user’s first interaction is with the privacy banner. On both the Home and Search pages, the banner shown to non-EU users &lt;strong&gt;re-renders the whole page when closed&lt;/strong&gt;. The issue was quite straightforward: the React state to render it or not was stored at the page level, way too high in the React tree. An intermediate component containing only the privacy banner logic (to save the preference in the local storage, then hide the banner) was enough to improve INP.&lt;/p&gt;
&lt;h3&gt;
  
  
  Avoiding the tooltip to re-render the whole page (INP decreased by 10 ms)
&lt;/h3&gt;

&lt;p&gt;We noticed something weird: the first interaction with the Search page (not the Home page) is slow. It didn’t matter what you interacted with — the first interaction with the page was always slow, but subsequent interactions were faster. The real aha moment was when we realized that even touching a &lt;strong&gt;non-interactive element&lt;/strong&gt; (like the page’s background) had a bad INP.&lt;/p&gt;

&lt;p&gt;How we proceeded:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Through the browser’s performance tools, we recorded an interaction that clearly showed the issue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The recording showed the issue was with the &lt;code&gt;touchstart&lt;/code&gt; event only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We removed all the &lt;code&gt;touchstart&lt;/code&gt; handlers on the main DOM elements (&lt;code&gt;html&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;&amp;lt;div id="__next"&amp;gt;&lt;/code&gt;) one by one.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2F1vguz7m4a2ph3b8cxagf.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%2F1vguz7m4a2ph3b8cxagf.png" alt="The browser dev tools visualize the event handlers, allow you to remove them, and jump to the listening JS module. Initially, there were more than 30 listeners." width="800" height="443"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The browser dev tools visualize the event handlers, allow you to remove them, and jump to the listening JS module. Initially, there were more than 30 listeners.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;All the handlers were responsible for part of the slow INP, but one of them caused the whole page’s re-render.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the Event Listener tab, the browser dev tools initially pointed us to Sentry’s listeners (which can also be used to measure Web Vitals, by the way).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After disabling Sentry, the issue remained, and the Event Listener tab pointed us to a custom tooltip we built on top of &lt;a href="https://www.radix-ui.com/primitives/docs/components/tooltip" rel="noopener noreferrer"&gt;Radix UI’s Tooltip&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason was a basic TypeScript oversight:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The initial tooltip’s state was &lt;code&gt;const [open, setOpen\] = useState(props?.open)&lt;/code&gt; which means open is &lt;code&gt;boolean | undefined&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;onClickOutside&lt;/code&gt; handler (called when you click anywhere) did &lt;code&gt;setOpen(false)&lt;/code&gt;. So open switched from &lt;code&gt;undefined&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;, resulting in a children re-render.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We fixed the issues just by changing the initial state &lt;code&gt;const [open, setOpen] = useState(props?.open ?? false)&lt;/code&gt;, removing &lt;code&gt;undefined&lt;/code&gt; from the possible states.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a rule of thumb, I always suggest “help TypeScript to help you” by reducing the possible types: &lt;a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types" rel="noopener noreferrer"&gt;Unions&lt;/a&gt; instead of generic types, &lt;a href="https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions" rel="noopener noreferrer"&gt;Discriminated Unions&lt;/a&gt; instead of optional properties when possible, no falsy types mixed with booleans, etc. You can read more about this topic in my “&lt;a href="https://dev.to/noriste/how-i-ease-the-next-developer-reading-my-code-1986"&gt;How I ease the next developer reading my code&lt;/a&gt;” article.&lt;/p&gt;
&lt;h3&gt;
  
  
  Updating Usercentrics for the EU privacy policy banner (INP decreased by 5 ms for the Search page, 40 ms for the Home page)
&lt;/h3&gt;

&lt;p&gt;For the EU users, we leverage &lt;a href="https://usercentrics.com/" rel="noopener noreferrer"&gt;Usercentrics&lt;/a&gt;. Specifically, we were using the V2 version. Similar to the previous point, the privacy banner was okay from an INP perspective, apart from when you closed it without accepting/changing privacy preferences. Closing the banner executed a huge amount of JS code that was out of our control and impacted many of Preply’s users.&lt;/p&gt;

&lt;p&gt;The Usercentrics team suggested we migrate to their V3, which solved this INP issue. But to do that, we also needed to update the GTM (Google Tag Manager) version to load the Usercentrics banner. We teamed up with those responsible for GTM and helped them with the migration, dropping another INP offender that impacted all of Preply.com’s pages.&lt;/p&gt;

&lt;p&gt;It’s worth noting that updating Usercentrics significantly impacted the Home page, where the privacy banner is one of only a few interactive elements. However, the effect on the highly interactive Search page was minimal.&lt;/p&gt;
&lt;h3&gt;
  
  
  Re-optimizing what we already optimized (INP decreased by 15 ms)
&lt;/h3&gt;

&lt;p&gt;The improvements mentioned above eliminated the worst INP. Then, we had a few weeks (see the graph) with no noticeable improvements. We were hitting the end of the 80/20 Pareto rule, and all the next fixes would have brought lower benefits while requiring more implementation time (which we didn’t have, the project was meant to be completed in 2024).&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%2Fmbzbzqy0d6fz5tdjhmzx.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%2Fmbzbzqy0d6fz5tdjhmzx.png" alt="The INP graph shows that after many quick wins, we had no improvements for a few weeks." width="800" height="242"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The INP graph shows that after many quick wins, we had no improvements for a few weeks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We then decided to return to the already optimized issues and optimize them even more. Ultimately, they proved to be low-hanging fruit because they impacted the core user flows, why not optimize those flows even more?! So &lt;strong&gt;we started using a 20x CPU slowdown&lt;/strong&gt; to measure performance improvements, which is surely closer to the budget devices we are optimizing for compared to the previous 6x slowdown (a 6x slowed-down high-level MacBook Pro is still faster than most devices on the market, especially when speaking of mobile devices).&lt;/p&gt;

&lt;p&gt;It worked, and we could push down INP even more and match the project’s OKRs 🎉.&lt;/p&gt;
&lt;h3&gt;
  
  
  Migrating the Home page to the App router (INP decreased by 10 ms on the Home page)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="http://next.js" rel="noopener noreferrer"&gt;Next.js App Router&lt;/a&gt; and RSC (&lt;a href="https://react.dev/reference/rsc/server-components" rel="noopener noreferrer"&gt;React Server Components&lt;/a&gt;) improved INP only slightly. We expected more, but it was a mistake of ours: in our initial POC with App Router, we wrongly used &lt;a href="https://web.dev/articles/tbt" rel="noopener noreferrer"&gt;TBT (Total Blocking Time)&lt;/a&gt; as a proxy metric for INP, that’s why our expectations were erroneous.&lt;/p&gt;

&lt;p&gt;But there’s more to tell! Just saying “App Router improved slightly” is a bit unfair because its potential is higher. Why? Well, I told you that we &lt;strong&gt;migrated, not rewritten, the Home page&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;So, we built a POC with a greenfield Next.js app. We chose only RSC-compatible libraries (for internationalization, for example), and rewritten part of the page with data-caching in mind. The results (performance is measured with a 3G network and 20x CPU throttling, locally, in prod mode) are &lt;strong&gt;impressive&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pages Router&lt;/th&gt;
&lt;th&gt;App Router&lt;/th&gt;
&lt;th&gt;Improvement %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JS bundle size&lt;/td&gt;
&lt;td&gt;1.2 MB&lt;/td&gt;
&lt;td&gt;173 KB&lt;/td&gt;
&lt;td&gt;85% less JS shipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML response time (shows how fast the server generates and sends the HTML)&lt;/td&gt;
&lt;td&gt;51.8 s&lt;/td&gt;
&lt;td&gt;15.7 s&lt;/td&gt;
&lt;td&gt;70% faster server execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Vitals: FCP&lt;/td&gt;
&lt;td&gt;5.8 s&lt;/td&gt;
&lt;td&gt;1.9 s&lt;/td&gt;
&lt;td&gt;65% better&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Vitals: LCP&lt;/td&gt;
&lt;td&gt;5.8 s&lt;/td&gt;
&lt;td&gt;2.8 s&lt;/td&gt;
&lt;td&gt;50% better&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;​​Web Vitals: INP&lt;/td&gt;
&lt;td&gt;Most interactions: 80-100 ms&lt;/td&gt;
&lt;td&gt;Most interactions: 30-40 ms&lt;/td&gt;
&lt;td&gt;60% better&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These results give justice to the App Router! Then, we all agreed that the optimal and long-term solution (not feasible in the project’s lifespan) would be to &lt;strong&gt;start from a fresh Next.js project and rewrite all the features&lt;/strong&gt; hacked into Next.js in years and years of fast growth.&lt;/p&gt;
&lt;h3&gt;
  
  
  useTransition, await-interaction-response, etc.
&lt;/h3&gt;

&lt;p&gt;During the project, we tried to go deep, find the root cause of the issues, and fix them. We didn’t want to hide the dust under the carpet and worsen the problem.&lt;/p&gt;

&lt;p&gt;Despite that, we encountered situations where the lack of proper design wasn’t fixable in the project’s lifespan, and “lying” to the browser was the only way to speed up some interactions. What I mean by “lying”:&lt;/p&gt;

&lt;p&gt;In the great &lt;a href="https://vercel.com/blog/demystifying-inp-new-tools-and-actionable-insights" rel="noopener noreferrer"&gt;Demystifying INP: New tools and actionable insights&lt;/a&gt; article, Vercel (Next.js’ creators) shared the &lt;a href="https://github.com/vercel-labs/await-interaction-response" rel="noopener noreferrer"&gt;await-interaction-response&lt;/a&gt; package. This is the source code:&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="cm"&gt;/**
 * Returns a promise that resolves in the next frame.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;interactionResponse&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;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Fallback for the case where the animation frame never fires.&lt;/span&gt;
    &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;Essentially, it’s a small hack to avoid blocking the main browser’s thread and postponing the execution of your heavy code. This improves INP, of course, but in our case, it worsens the problem because it’s a sort of “Don’t worry, the lack of design and development anti-patterns are fine, just use this workaround” message, which we don’t want to spread.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://react.dev/reference/react/useTransition" rel="noopener noreferrer"&gt;React’s useTransition&lt;/a&gt; is more elegant because it’s embedded straight in the framework, but in our case, using it hides the root problems instead of helping/teaching how not to introduce them upfront and removing the existing performance bottlenecks.&lt;/p&gt;

&lt;p&gt;But for those looking for short-term and quick solutions… well, keep them in mind 😅.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just one note:&lt;/strong&gt; if you are considering implementing a custom yielder, please don’t rely on &lt;code&gt;requestIdleCallback&lt;/code&gt;! When working on the Design System Visual Coverage, we saw that the browser frequently invokes the passed callback after several seconds due to other intensive processes. In &lt;a href="https://medium.com/preply-engineering/the-implementation-details-of-preplys-design-system-visual-coverage-86b4a78ad2bb" rel="noopener noreferrer"&gt;The Implementation Details of Preply’s Design System Visual Coverage&lt;/a&gt; article, you can read all the performance optimizations we made there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leveraging React’s useDeferredValue (no INP improvements)
&lt;/h3&gt;

&lt;p&gt;We were a bit surprised this didn’t work. &lt;a href="https://react.dev/reference/react/useDeferredValue" rel="noopener noreferrer"&gt;React’s useDeferredValue&lt;/a&gt; keeps input fields snappy when the result of the users’ typing (the list of languages to learn, in our case) requires a significant amount of time to be updated on the page. After refactoring one of our input fields, we noticed no improvements compared to when the input field’s value was debounced. But why?&lt;/p&gt;

&lt;p&gt;We realized that useDeferredValue works as a charm when the items to render are heavy at the React rendering phase (essentially, when React executes the component’s functions that return the JSX) but soft on the DOM reconciliation phase (where React adds/updates/removes the elements from the DOM). Our case was the opposite, the components are fast enough at rendering but &lt;strong&gt;heavy regarding how many DOM elements they produce&lt;/strong&gt; (you can read &lt;a href="https://x.com/NoriSte/status/1870024505940189351" rel="noopener noreferrer"&gt;a quick thread on X&lt;/a&gt; about this), vanishing useDeferredValue’s benefits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimizing the Design System components (no INP improvements)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Path (Preply’s Design System) is used a lot&lt;/strong&gt;: the Search page counts 875 usages of the Path’s components, and 76% of what the users see comes from Path; you can dig more into how we collect the stats in the Visual coverage: &lt;a href="https://medium.com/preply-engineering/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-1057115f4aff" rel="noopener noreferrer"&gt;Why and How Preply Measures the Impact of the Design System&lt;/a&gt; article. A particularly heavy part of the Search page’s render tree highlighted that the two most used Path’s components (LayoutFlex and Text) had room for improvement when transforming the React props into CSS class names, impacting the overall page’s INP.&lt;/p&gt;

&lt;p&gt;Refactoring the Path’s components doesn’t happen overnight, and we spent days heavily testing and refactoring the LayoutFlex component to avoid any regressions across the whole website. All this heavy work created the foundations for how the Design System team can safely test and refactor the components, but from an INP perspective, &lt;strong&gt;it brought no improvements&lt;/strong&gt;. Our analysis was wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up the Search page’s code (no INP improvements)
&lt;/h3&gt;

&lt;p&gt;The hundreds of experiments the Product teams launched in recent years were still part of the codebase. Some were just dead code, some were imported dynamically only if the users were in one of the a/b cases, but never removed when the experiments were scaled or killed. Some were always imported, negatively impacting the JS bundle.&lt;/p&gt;

&lt;p&gt;We &lt;strong&gt;removed 15% of the Search page codebase&lt;/strong&gt; (7K lines of code) for two weeks, but this didn’t show INP improvements. This has been a recurring theme during the project (later on, we speak about the Next.js Ap Router), but shipping less JS never improved INP for our Search page (please note I’m speaking only of INP, shipping less JS is always good).&lt;/p&gt;

&lt;h3&gt;
  
  
  Quickly optimizing Preply Chat for the logged-in users (no INP improvements)
&lt;/h3&gt;

&lt;p&gt;Logged-in users can access Preply Chat from the header, a modal that opens inside the same page. From Hotjar, we got the idea that many users were opening the Preply Chat.&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%2Fi6kqfrxlper3guhuocxw.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%2Fi6kqfrxlper3guhuocxw.png" alt="A screenshot of the Search page shows the Chat button at the top for logged-in users." width="676" height="758"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A screenshot of the Search page shows the Chat button at the top for logged-in users.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We didn’t expect it, given how indexed pages and heavy SEM campaigns bring tons of new and not logged-in users to the Search page, but from some quick queries, we realized 80% of the Search page views come from logged-in users 🤯. Given that Preply Chat is complex, we used the mentioned await-interaction-response, postponing some of the Preply Chat actions. We didn’t see the INP improvements we expected, meaning that our assumptions from intersecting all the data were again wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removing DataDog RUM (no INP improvements)
&lt;/h3&gt;

&lt;p&gt;After extensive local tests, and even if it looked weird, DataDog RUM seemed to be causing worse INP. Luckily, the tool was redundant for us (since we mostly rely on the web-vitals library), and we could remove it. Once more, after doing so, we didn’t notice any improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offloading the third-party scripts with Partytown (we gave up)
&lt;/h3&gt;

&lt;p&gt;We load many third-party scripts, some in a developer-controlled way and ~40 through &lt;a href="https://marketingplatform.google.com/intl/it/about/tag-manager/" rel="noopener noreferrer"&gt;GTM (Google Tag Manager)&lt;/a&gt; without developer control. Despite knowing that the scripts loaded through GTM impact the page negatively, we gave up on trying to move them to Web Workers with &lt;a href="https://partytown.builder.io/" rel="noopener noreferrer"&gt;Partytown&lt;/a&gt;. Here’s why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Once loaded by GTM, some scripts append more scripts to the DOM, which are executed in the main thread, partially defeating the purpose of adopting Partytown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Years and years of scripts added through GTM, and maybe frameworks/integrations built on top of them, almost guarantee that we break something without a clear correlation between what we break and what results are wrong.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Given how we all depend on our data and the short amount of time available for the project, we decided to give up, given the unclear return on investment of this work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jumping on the new React Compiler (we didn’t even try)
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://react.dev/learn/react-compiler" rel="noopener noreferrer"&gt;React Compiler&lt;/a&gt; is under construction at the time of this writing. However, the limitations we discussed with the Devex team were the Next.js v14-&amp;gt;v15 upgrade and the React v18-&amp;gt;v19 upgrade, which required more time than ours and didn’t fit the project’s time scope. &lt;a href="https://github.com/reactwg/react-compiler/discussions/52?ck_subscriber_id=2236371854&amp;amp;utm_source=convertkit&amp;amp;utm_medium=email&amp;amp;utm_campaign=%E2%9A%9B%EF%B8%8F%20This%20Week%20In%20React%20#217:%20View%20Transitions,%20RSC,%20Next.js,%20SWR,%20Nextra,%20React%20Router,%20Rails,%20Compiler,%20EAS%20Hosting,%20Shopify,%20Skia,%20OTP,%20Gesture%20Handler,%20Radon,%20React-Query,%20TC55,%20Bun,%20CSS...%20-%2016284665" rel="noopener noreferrer"&gt;Wakelet shared an early feedback&lt;/a&gt; about the Web Vitals improvements they got from adopting the new compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did all this work also improve the UX?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The answer is: YES!&lt;/strong&gt; Working with 6x and 20x CPU slowdowns, the UX improvements are evident, though there’s still room for further enhancements. INP proves to be a great metric from this perspective; it acts as a true litmus test for how painful it is to use your product when users don’t have access to top-notch devices. While all Web Vitals are valuable, INP uniquely measures the user experience throughout the entire session, regardless of whether the user’s journey is brief and superficial or deep and long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Miscellaneous
&lt;/h2&gt;

&lt;p&gt;Many of the project’s details have been omitted from this article because they are not crucial or relevant to readers. Let me just mention some quick facts/suggestions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We saw the first INP improvement after more than one month of work, out of a quarter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The weekends were crucial to measure the impact of our work because we saw &lt;strong&gt;dashboards needed 24–48 hours to reflect the INP changes&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We couldn’t stop other Product teams from running experiments on the pages due to an overlapping important Product goal. It wasn’t optimal, but the generated friction wasn’t so high.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Devex team worked hard to upgrade Next.js from 13 to 14 and leverage the faster Turbopack for local development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The project started with the goal of optimizing both INP and TTFB. Then the two metrics were split into two different projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Jumping on the App Router required us to refactor all the LESS modules to SCSS (also in the Design System).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Always &lt;strong&gt;validate your assumptions on more than one PC&lt;/strong&gt;: it happened more than once that one of us identified possible improvements, but the same improvements were invisible on other laptops (because of background zombie processes, because a user becomes part of a data sampling that slows down the website, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  Medium and long-term plans
&lt;/h2&gt;

&lt;p&gt;All the mentioned improvements are short-term, and the benefits can vanish quickly if we don’t make performance a first-class citizen when developing the Product. We haven’t formalized all the activities for this goal, but we have some in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;(you saw it in action through this article) Monitor Web Vitals to catch regressions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(already done) Add Web Vitals to our experiments framework’s dashboard and the other Product metrics we all care about.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add performance data to fill the PR template.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(already in progress a the time of writing) Run workshops to share the takeaways and spread React best practices with all our frontenders.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run some workshops to share how to deal with the existing Product’s refactors from a performance perspective.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Discuss the role of App Router in our product and our infrastructure around it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;✅ Tracking tons of RUM (Real User Monitoring) data and having it at hand is key.&lt;/p&gt;

&lt;p&gt;✅ Adopting a &lt;strong&gt;100% data-driven approach&lt;/strong&gt; from the beginning is crucial. As you can see above, the optimizations that solved the INP problems are not rocket science, but &lt;strong&gt;it is important to identify them before jumping on the code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;✅ Don’t blindly follow what others did and shared (including this article); don’t take for granted that the other, more hyped framework solves your problem. Ultimately, &lt;strong&gt;we achieved our INP goals by speeding up our existing code&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some more resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://vercel.com/blog/demystifying-inp-new-tools-and-actionable-insights" rel="noopener noreferrer"&gt;Demystifying INP: New tools and actionable insights&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://open.nytimes.com/enhancing-the-new-york-times-web-performance-with-react-18-d6f91a7c5af8" rel="noopener noreferrer"&gt;Enhancing The New York Times Web Performance with React 18&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://kurtextrem.de/posts/improve-inp-react" rel="noopener noreferrer"&gt;How To Improve INP: React&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The great people who worked on the page speed project
&lt;/h3&gt;

&lt;p&gt;This project took a village, and I want to thank everyone involved 🤗&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://www.linkedin.com/in/maksym-ridush/" rel="noopener noreferrer"&gt;Maksym Ridush&lt;/a&gt;, for the initial deep analysis that resulted in the page speed project 👏.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the team responsible for the Search page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/artem-yermak-9671745b/" rel="noopener noreferrer"&gt;Artem Yermak&lt;/a&gt;, thank you for the endless support and for improving INP 💪.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/ivan-biletskyi-5406a5199/" rel="noopener noreferrer"&gt;Ivan Biletskyi&lt;/a&gt;, for the outstanding support, reviews, and knowledge-sharing ❤️.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the team responsible for the Home page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/carlesbaas/" rel="noopener noreferrer"&gt;Carles Ballester&lt;/a&gt;, for the Home page migration and working on the endless experimentation issues 🤘(and &lt;a href="https://www.linkedin.com/in/rserratm/" rel="noopener noreferrer"&gt;Robert Serrat Morros&lt;/a&gt; for supporting Carles).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/michaelcastro1/" rel="noopener noreferrer"&gt;Michael Castro&lt;/a&gt;, the Engineering Manager who’s also a front-end expert you don’t expect 😊&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/alejandro-lampropulos/" rel="noopener noreferrer"&gt;Alejandro Lampropulos&lt;/a&gt;, for the GTM and UC support 🙏.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Preply’s Design System’s engineers, who have been borrowed to the page speed project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/lightespresso/" rel="noopener noreferrer"&gt;Oleksandr Kozlov&lt;/a&gt;, the real Next.js guru 🤯.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/seifkamal/" rel="noopener noreferrer"&gt;Seif Kamal&lt;/a&gt;, who constantly challenges and analyzes every single data 🚀.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/noriste/" rel="noopener noreferrer"&gt;Myself&lt;/a&gt; 😁.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/bogdan-brindusan/" rel="noopener noreferrer"&gt;Bogdan Brindusan&lt;/a&gt;, for jumping on and managing such a complex and out-of-the-comfort-zone project.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/vadym-vlasenko-699a9222/" rel="noopener noreferrer"&gt;Vadym Vlasenko&lt;/a&gt;, for always digging into the details and challenging us.&lt;/p&gt;

&lt;p&gt;All the people from different teams who helped us&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/matepapp/" rel="noopener noreferrer"&gt;Mate Papp&lt;/a&gt; for everything DevEx, including the Next.js v14 migration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/kirill-sablin-b51a8428/" rel="noopener noreferrer"&gt;Kyryl Sablin&lt;/a&gt;, from the SRE team, thank you for the constant help and support.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, of course, the reviewers who helped make this article better 🤗&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/sebastienlorber/" rel="noopener noreferrer"&gt;Sébastien Lorber&lt;/a&gt;, for the suggestions, comments, and your infinite news source: &lt;a href="https://thisweekinreact.com/" rel="noopener noreferrer"&gt;This Week in React&lt;/a&gt; ⚛️.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://x.com/beaussan" rel="noopener noreferrer"&gt;Nicolas Beaussart&lt;/a&gt;, for the detailed review and the time you always dedicate to my articles 😳.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/ajessop/" rel="noopener noreferrer"&gt;Andy Jessop&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/omri-lavi/" rel="noopener noreferrer"&gt;Omri Lavi&lt;/a&gt;, for your interest and comments 🤗.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/blv-dmitry/" rel="noopener noreferrer"&gt;Dmitry Belyaev&lt;/a&gt;, for reading this even if you were on vacation 🌴.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/charca/" rel="noopener noreferrer"&gt;Maxi Ferreira&lt;/a&gt;, the author of one of my favourite newsletter: &lt;a href="https://frontendatscale.com" rel="noopener noreferrer"&gt;Frontend at Scale&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/adevnadia/" rel="noopener noreferrer"&gt;Nadia Makarevich&lt;/a&gt;, the &lt;a href="https://www.advanced-react.com/" rel="noopener noreferrer"&gt;Advanced React&lt;/a&gt; course’s creator and &lt;a href="https://www.developerway.com/" rel="noopener noreferrer"&gt;Developer Way&lt;/a&gt;’s author.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From improving our engineering culture to becoming an &lt;strong&gt;AI-native company&lt;/strong&gt;… Would you like to join us and work in a purpose-driven organization where work, growth, and learning happen at the same time? Preply continues growing and we are actively looking for talented candidates to join our Engineering team! If you are excited about taking on a new challenge, &lt;a href="https://preply.com/en/careers?team=Engineering&amp;amp;utm_source=medium-wearepreply&amp;amp;utm_medium=blog-post&amp;amp;utm_campaign=2021-EB-Frank-Tobias-Story" rel="noopener noreferrer"&gt;check out our open positions here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>webperf</category>
      <category>webvitals</category>
    </item>
    <item>
      <title>The Implementation Details of Preply’s Design System Visual Coverage (part II)</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Thu, 03 Oct 2024 11:32:58 +0000</pubDate>
      <link>https://forem.com/noriste/the-implementation-details-of-preplys-design-system-visual-coverage-part-ii-1ao2</link>
      <guid>https://forem.com/noriste/the-implementation-details-of-preplys-design-system-visual-coverage-part-ii-1ao2</guid>
      <description>&lt;p&gt;&lt;em&gt;This article has been originally posted &lt;a href="https://medium.com/preply-engineering/the-implementation-details-of-preplys-design-system-visual-coverage-86b4a78ad2bb" rel="noopener noreferrer"&gt;on Preply's engineering blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Implementing the coverage algorithm, working on the process that resulted in having the dashboard, working with teams to increase the coverage, implementing it for the App, etc., has required months of work. In this (very long) article, we will discuss all the project’s technical details. We also made &lt;strong&gt;the visual coverage code available on GitHub&lt;/strong&gt; at &lt;a href="https://github.com/preply/design-system-visual-coverage" rel="noopener noreferrer"&gt;preply/design-system-visual-coverage&lt;/a&gt;, so you can use it for your product too.&lt;/p&gt;

&lt;p&gt;If you are new to the topic, please read &lt;a href="https://dev.to/noriste/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-part-i-3omb"&gt;the first non-technical article&lt;/a&gt; explaining the whys and hows behind the design system visual coverage project.&lt;/p&gt;




&lt;p&gt;You may have heard of the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the &lt;a href="https://www.intodesignsystems.com/" rel="noopener noreferrer"&gt;Into Design Systems conference&lt;/a&gt;: here you can find &lt;a href="https://intodesignsystems.substack.com/p/measuring-design-system-impact-lessons" rel="noopener noreferrer"&gt;the article they wrote about it&lt;/a&gt;, and &lt;a href="https://www.figma.com/community/file/1509535015145685735/design-system-visual-coverage-resources" rel="noopener noreferrer"&gt;the resources we shared&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;At the &lt;a href="https://www.jsday.it/" rel="noopener noreferrer"&gt;JSDay&lt;/a&gt; conference: here is &lt;a href="https://www.jsday.it/talks_speakers/#DesignSystemVisualCoverageInWebAndApp(ReactNative)Applications" rel="noopener noreferrer"&gt;the talk description&lt;/a&gt;, and the &lt;a href="https://cef62.github.io/design-system-coverage-jsday-2025" rel="noopener noreferrer"&gt;slides&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;At one of &lt;a href="https://www.belkadigital.com/" rel="noopener noreferrer"&gt;Belka&lt;/a&gt;’s AMA: here is &lt;a href="https://www.youtube.com/watch?v=Kw_eGDppG4M" rel="noopener noreferrer"&gt;the recording&lt;/a&gt; (in Italian).&lt;/li&gt;
&lt;li&gt;At one of the &lt;a href="https://podcasts.apple.com/it/podcast/commit-to-growth/id1788326535" rel="noopener noreferrer"&gt;Commit to Growth&lt;/a&gt;’s episodes: here is &lt;a href="https://podcasts.apple.com/us/podcast/lessons-from-large-codebases-design-visual-coverage/id1788326535?i=1000688799413" rel="noopener noreferrer"&gt;the recording&lt;/a&gt;. The podcast is curated by one of Preply’s engineers: 
&lt;a href="https://medium.com/u/754df0a2b156?source=post_page---user_mention--1057115f4aff---------------------------------------" rel="noopener noreferrer"&gt;Yasemin çidem&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Web implementation
&lt;/h1&gt;

&lt;p&gt;The visual coverage performs two main operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Parsing the DOM to gather all the elements and their boundaries&lt;/li&gt;
&lt;li&gt; Creating a bitmap out of the element boundaries and calculating the coverage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As explained in &lt;a href="https://dev.to/noriste/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-part-i-3omb"&gt;the first article&lt;/a&gt;, we measure the visual coverage on users’ devices. Browsers provide all the APIs we need to avoid getting into the user’s UX: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback" rel="noopener noreferrer"&gt;requestIdleCallback&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" rel="noopener noreferrer"&gt;Web Workers&lt;/a&gt;. The following graph shows how we decided to run the calculation.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2A7QxNL3Qgeb-D7C9l" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2A7QxNL3Qgeb-D7C9l" alt="This is a timeline graph showing when and how we want to retrieve all the DOM element properties (when the user is idle) and that the heavy pixel count happens in a Web Worker." width="1316" height="1600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This timeline graph shows when and how we want to retrieve all the DOM element properties (when the user is idle) and that the heavy pixel count happens in a Web Worker.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Some of our pages are SSR’ed; others are part of a regular SPA; we are exploring using RSC… But from a visual coverage perspective, it doesn’t matter. We measure the visual coverage on the client, on all the user’s devices every five minutes (at .00, .05, .10, and so on), across all Preply.com’s pages, regardless of what the users are doing (scrolling the page or taking an online lesson). At the moment, we are collecting 150K-350 K events per day. This high number of events guarantees that every single Preply.com page is tracked.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preparatory steps
&lt;/h2&gt;

&lt;p&gt;Step 1: We updated all the Design System components to include a dedicated DOM attribute: &lt;em&gt;data-preply-ds-component&lt;/em&gt;, whose value is the name of the React component. This allows us to distinguish Design System DOM elements from the other ones. Also, this detaches the coverage script from the React.js nature of the website, and dedicated build and deployment steps were not needed.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AR_vcK9KmVaBbr5se" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AR_vcK9KmVaBbr5se" alt="A screenshot of the browser devtools showing the HTML attributes of some Preply.com's elements." width="1600" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Inspecting Preply.com’ pages reveals all the design system component data attributes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Create the visual coverage containers. In our case, this translated into setting the &lt;em&gt;data-preply-ds-coverage&lt;/em&gt; attribute to all the pages, assigning them a name and the team the page belongs to. In the JSON, the page is generically called &lt;em&gt;component&lt;/em&gt; because the same logic applies to smaller components (like the unified header, the calendars, the chat widget, etc.) and allows splitting the responsibility of the same page across different teams.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AG6nlmnSriJOQxYGa" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AG6nlmnSriJOQxYGa" alt="A screenshot of the browser devtools showing the HTML attributes of some Preply.com's elements." width="1600" height="586"&gt;&lt;/a&gt;&lt;br&gt;
Inspecting Preply.com’ pages reveals all the design system coverage container data attributes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using a bitmap to recreate the page’s pixels
&lt;/h2&gt;

&lt;p&gt;Counting the number of colored pixels in the DOM is a slow operation, even if you work with a canvas (we haven’t tried &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas" rel="noopener noreferrer"&gt;OffscreenCanvas&lt;/a&gt;, though). So, we thought of a way to “recreate” the page with simple data we could mutate and aggregate quickly. An array of colors (technically speaking: a bitmap) would be perfect!&lt;/p&gt;

&lt;p&gt;The idea is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Traversing the whole DOM reading all the bounding rects (top, left, width, height) of every element.&lt;/li&gt;
&lt;li&gt; Creating a bi-dimensional array in which every item represents whether a pixel of the page comes from the Path Design System or not.&lt;/li&gt;
&lt;li&gt; Counting the pixels in the array.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To show how the bi-dimensional array could look, consider a hypothetical 20x20 pixels page with some flex containers, a heading, two buttons, and a footer that are Path Design System components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  [🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥], // &amp;lt;-- non-DS components' borders are red
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟥],
  [🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥],
  [⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️],
  [🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥],
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟥], 
  [🟥,⬜️,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,⬜️,🟥],
  [🟥,⬜️,🟩,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩,⬜️,🟥],
  [🟥,⬜️,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,⬜️,🟥],
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟥],
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩,🟩,🟩,🟩,🟩,⬜️,🟩,🟩,🟩,🟩,🟩,⬜️,🟥], // &amp;lt;-- DS components' borders are green
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩,⬜️,⬜️,⬜️,🟩,⬜️,🟩,⬜️,⬜️,⬜️,🟩,⬜️,🟥],
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩,🟩,🟩,🟩,🟩,⬜️,🟩,🟩,🟩,🟩,🟩,⬜️,🟥],
  [🟥,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟥],
  [🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥,🟥],
  [🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩],
  [🟩,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩], // &amp;lt;-- The rest of the page is white
  [🟩,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩],
  [🟩,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,⬜️,🟩],
  [🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩,🟩],
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the sake of simplicity, the example shows a 1-pixel border for all the components. In reality, the component weights (see &lt;a href="https://dev.to/noriste/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-part-i-3omb"&gt;the previous article&lt;/a&gt;’s chapter about the component weights) decide the width of the border.&lt;/p&gt;

&lt;p&gt;A bitmap is generated for each visual coverage container. In the case of nested containers (like a page that belongs to team A but contains a component that belongs to team B), the area of the nested container looks empty in the parent container’s bitmap.&lt;/p&gt;

&lt;p&gt;Please note: there are two approximations in the DOM-&amp;gt;array transformation. The algorithm does not respect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;CSS stacking contexts&lt;/strong&gt; (aka z-index): every element is above the previous one. This isn’t a big deal, given the nature and structure of Preply.com’s pages (which have a document-like structure, rarely relying on z-index, apart from the Classroom).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Content overflow&lt;/strong&gt;: this limitation can be easily overcome by using containers, which limit the boundaries of the children.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We haven’t tested the visual coverage code on heavy fixed, and non-scrollable UIs. If you do, please let us know 😊.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance optimizations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We quickly validated the idea with a throwable POC&lt;/strong&gt; (this is where ChatGPT shines). Still, we stepped into severe performance optimizations to move from the initial 150 blocking milliseconds for a tall page (on my machine™) to the final 50 non-blocking milliseconds. Also, we moved from 30 MB to 300 KB regarding memory footprint. To achieve these improvements, we also involved &lt;a href="https://www.linkedin.com/in/massimilianomantione" rel="noopener noreferrer"&gt;Massimiliano Mantione&lt;/a&gt;, a &lt;a href="https://www.linkedin.com/in/massimilianomantione/" rel="noopener noreferrer"&gt;former Google Chrome V8 Engineer&lt;/a&gt;, and &lt;a href="https://www.linkedin.com/in/matteoronchi/" rel="noopener noreferrer"&gt;Matteo Ronchi&lt;/a&gt; from &lt;a href="https://www.workwave.com/" rel="noopener noreferrer"&gt;WorkWave&lt;/a&gt; (software architect), who is now using the same visual coverage approach on a very different-from-Preply product.&lt;/p&gt;

&lt;p&gt;We applied some obvious optimizations (like skipping SVG contents or hidden elements) and some less obvious ones, like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The array should be monodimensional instead of bi-dimensional&lt;/strong&gt;. This optimizes memory access since accessing a memory address to find another memory address (required to access an item of the array stored inside another array) is not optimal for the CPU.&lt;/li&gt;
&lt;li&gt; Every item of the array must use the minimum possible number of bits. Strings are wrong from this point of view because their memory usage is dynamic by default. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays" rel="noopener noreferrer"&gt;&lt;strong&gt;Typed Arrays&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;is the best choice&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; If you write branches-free code (if conditions are branches), the JIT compiler can optimize your code, resulting in C++-like performances.&lt;/li&gt;
&lt;li&gt; The “count the pixels” code could be moved to a Web Worker and run without interruptions. This does not greatly improve performance but significantly impacts the code’s readability. The equivalent calculation made through requestIdleCallback forces you to write interruptible code, which is inevitably way harder to read than straightforward synchronous code running in a Web Worker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All the above allowed us to reduce a lot the execution time, and some tests on &lt;strong&gt;old devices&lt;/strong&gt; showed the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; OnePlus 6 (top-notch 2018 phone): 650ms.&lt;/li&gt;
&lt;li&gt; Huawei P9 (cheap 2016 phone): 900ms. Interestingly, the Huawei P9 (4x times slower in everyday use) performs &amp;lt;50% worse than the OnePlus 6. The hardware is okay with this kind of browser operation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Is it necessary to make the script 100% non-blocking? The answer is yes!&lt;/strong&gt; Our initial implementation included a very small blocking phase. Still, two users (out of the 1% Preply users included in the initial experiment) faced a prolonged blocking phase, with a worst-case duration of more than 400ms. See the two spikes in our monitoring graph.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AR2tItipLPnP27A0W" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AR2tItipLPnP27A0W" alt="A graph showing the spike in the script's duration." width="1600" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By making the script asynchronous, we have to live with the fact that if we split reading the DOM over multiple idle callbacks, we could count the coverage on a half-changing page. We think this is tolerable, especially given that if the count is 100% non-blocking, we can run it whenever we want (also immediately after the React application is initialized), which &lt;strong&gt;increases the chances of intercepting users who quickly move through pages&lt;/strong&gt; (if you count the coverage infrequently, some pages could never get included in the stats).&lt;/p&gt;

&lt;p&gt;To limit this issue, we stop counting the coverage when the user clicks, when the URL changes, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample rate and events cardinality
&lt;/h2&gt;

&lt;p&gt;As mentioned, we measure the visual coverage on all the user’s devices every thirty minutes (at .00, and at .30), across all Preply.com’s pages, whatever the users are doing. At the moment of writing, we collect 150K-350K events per day. They are sent to a custom endpoint and forwarded to our paid DataDog.&lt;/p&gt;

&lt;p&gt;We don’t pay DataDog based on the number of events but their cardinality. The cardinality is inherently high because the most essential event data are the team (16, at the moment of writing) and the component (the page or component name. They are more than 100). We have sacrificed other data to reduce the event’s cardinality and not incur additional costs (like the user type, the release version, etc.).&lt;/p&gt;

&lt;h1&gt;
  
  
  App (React Native) implementation
&lt;/h1&gt;

&lt;p&gt;Let’s start with two considerations that differentiate tracking the coverage on Preply’s app compared to Preply.com:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; When we started working on the app, &lt;strong&gt;React Native didn’t provide handy performance-oriented APIs&lt;/strong&gt; like &lt;em&gt;requestIdleCallback&lt;/em&gt; or Web Workers. We can’t easily be 100% sure we don’t impact Preply’s users’ UX, so we don’t measure the Design System’s visual coverage in production.&lt;/li&gt;
&lt;li&gt; The app’s &lt;strong&gt;E2E tests cover 100%&lt;/strong&gt; of the use cases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Please note:&lt;/strong&gt; &lt;em&gt;requestIdleCallback&lt;/em&gt; for React Native’s new architecture was released in August 2024 (see &lt;a href="https://github.com/facebook/react-native/pull/44759" rel="noopener noreferrer"&gt;the PR&lt;/a&gt;) as part of React Native v0.75. After we upgrade the app to React Native v0.75.0, we can also release the coverage count for the app in production.&lt;/p&gt;

&lt;p&gt;We then leveraged the app’s E2E tests to count the coverage and send the results to DataDog.&lt;/p&gt;

&lt;p&gt;The majority of the implementation is the same as the Web counterpart. There are only two differences compared to the Web implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; How we retrieve the UI elements’ boundaries. By design, the web offers an open and parseable tree, but mobile apps don’t.&lt;/li&gt;
&lt;li&gt; How do we identify Path Design System components? This heavily depends on the first point.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After some experiments, we decided to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Leverage some &lt;a href="https://github.com/preply/design-system-visual-coverage/blob/main/production-code/app/ToolkitManager.swift" rel="noopener noreferrer"&gt;straightforward Swift code to gather all the elements’ boundaries&lt;/a&gt; (at the moment, we are counting the coverage only on iOS)&lt;/li&gt;
&lt;li&gt; Identify Path Design System components through React Native testID, which converts to iOS’ &lt;em&gt;accessibilityIdentifier&lt;/em&gt;. This also required updating our test utilities because &lt;em&gt;testID&lt;/em&gt; is used in the E2E tests to interact with some elements.&lt;/li&gt;
&lt;li&gt; Launch the coverage count from E2E tests through a dedicated deep link, one of the few options for triggering an internal function from an external testing tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What about view hierarchy files?
&lt;/h2&gt;

&lt;p&gt;We experimented using the iOS View Hierarchy. On paper, it would have worked (see screenshot) since it contains all the data we need, but unfortunately, it’s a binary file and can only be read if it is XCode.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2ATXGBrrPFKGDCQxkF" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2ATXGBrrPFKGDCQxkF" alt="A screenshot of XCode showing the view hierarchy of Preply's app." width="1600" height="885"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The view hierarchy of Preply’s App’s main screen contains all the data we need. However, this data is inaccessible outside of XCode.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Design System visual coverage score and dashboard
&lt;/h1&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%2Fjqynr06q1aynhvevuvwq.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%2Fjqynr06q1aynhvevuvwq.png" alt="The dashboard shows the current visual coverage, the average per team, the trends, etc." width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The official Preply’s Design System visual coverage dashboard.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We have two dashboards:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The official one&lt;/strong&gt; is used and referenced by everyone to check the current coverage, identify possible areas for improvement, and eventually set teams’ OKRs.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;A monitoring one is&lt;/strong&gt; used by the Design System team to identify errors, slower calculations, distribution over different devices, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  How We Test the Visual Coverage Code
&lt;/h1&gt;

&lt;p&gt;Only some key parts of the whole code are tested. This is enough for me to be sure the critical parts of the code work. The most exciting part is how the main web function (&lt;code&gt;createCalculateDsVisualCoverages&lt;/code&gt;) is tested: through &lt;a href="https://vitest.dev/guide/browser/" rel="noopener noreferrer"&gt;Vitest browser mode&lt;/a&gt;. Testing the visual coverage code based on such native but complex browser APIs (&lt;em&gt;requestIdleCallback&lt;/em&gt; and Web Workers) is key to writing reliable unit tests. You can check the test code &lt;a href="https://github.com/preply/design-system-visual-coverage/blob/main/packages/visual-coverage-web/src/createCalculateDsVisualCoverages.browser.test.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Apart from that detail, all the code heavily relies on dependency injection to test, which is the easiest possible way.&lt;/p&gt;

&lt;h1&gt;
  
  
  How Teams Can Play with the Visual Coverage
&lt;/h1&gt;

&lt;p&gt;We expose some utilities on the global object (window) to allow teams to play with the visual coverage. &lt;a href="https://github.com/preply/design-system-visual-coverage/blob/main/packages/visual-coverage-web/src/debug/exposeGlobalDsVisualCoverageObject.ts" rel="noopener noreferrer"&gt;exposeGlobalDsVisualCoverageObject is the function&lt;/a&gt; that exposes them. The most important ones are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  __&lt;em&gt;PREPLY_DS_COVERAGE.runAndLog()&lt;/em&gt;: it runs the calculation, it logs everything in the console, but it does not send calculation events to DataDog.&lt;/li&gt;
&lt;li&gt;  __&lt;em&gt;PREPLY_DS_COVERAGE.runAndVisualize()&lt;/em&gt;: The same as above, but also add some visible rectangles to the page.&lt;/li&gt;
&lt;li&gt;  __&lt;em&gt;PREPLY_DS_COVERAGE.runAndVisualizeContainer(componentName: string)&lt;/em&gt;: The same as above but only for a single container. This is particularly useful for modals.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;__PREPLY_DS_COVERAGE.reset()&lt;/strong&gt;: Removes the colored rectangles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, everywhere (on prod, staging, Storybook, locally), product teams can just launch __PREPLY_DS_COVERAGE.runAndVisualize() in the browser’s console to get the calculation.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test it on Your Product
&lt;/h1&gt;

&lt;p&gt;After reading the why behind this project in &lt;a href="https://dev.to/noriste/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-part-i-3omb"&gt;the initial article&lt;/a&gt;, and digging deeper into all the tech details here, the next step is to experiment with the visual coverage yourself. In the &lt;a href="https://github.com/preply/design-system-visual-coverage" rel="noopener noreferrer"&gt;preply/design-system-visual-coverage&lt;/a&gt; repository, we shared:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The entire implementation of visual coverage (we will update it periodically as we evolve it) lives in our design system repository.&lt;/li&gt;
&lt;li&gt; All the code that consumes the visual coverage APIs in the product and converts the data to DataDog.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try the same approach in your product, you tweak the code, etc., please share what worked and what did not for you 🤗&lt;/p&gt;

&lt;h1&gt;
  
  
  Special Thanks
&lt;/h1&gt;

&lt;p&gt;I want to publicly kudos all the people involved in this project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The whole design system team: &lt;a href="http://justine/" rel="noopener noreferrer"&gt;Justine&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/seifkamal/" rel="noopener noreferrer"&gt;Seif&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/lightespresso/" rel="noopener noreferrer"&gt;Alex&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/javierarques/" rel="noopener noreferrer"&gt;Javi&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/alisa-antypova/" rel="noopener noreferrer"&gt;Alisa&lt;/a&gt;. I love this team 😍&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/vadym-vlasenko-699a9222/" rel="noopener noreferrer"&gt;Vadym&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/ihor-kasianov-b58b783a/" rel="noopener noreferrer"&gt;Igor&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/joshcrossick/" rel="noopener noreferrer"&gt;Josh&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/dmytrovoloshyn/" rel="noopener noreferrer"&gt;Dmytro&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/dominika-szuka%C5%82a-kosi%C5%84ska-1240627a/" rel="noopener noreferrer"&gt;Dominika&lt;/a&gt;, for the support and feedback 😊&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/oleg-buiar/" rel="noopener noreferrer"&gt;Oleg&lt;/a&gt;, for creating the dashboard and helping with all the data.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/daniel-guerra-guerrero-a64180a9/" rel="noopener noreferrer"&gt;Daniel&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/albertovilva" rel="noopener noreferrer"&gt;Alberto&lt;/a&gt;, for the early internal feedback.&lt;/li&gt;
&lt;li&gt; All the Preply front-end engineers for supporting the design system, and this initiative. All the Preply app engineers for helping with the React Native implementation.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/jalvarez88/" rel="noopener noreferrer"&gt;Javier&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/serhii-tanskyi-243779143/" rel="noopener noreferrer"&gt;Serhii&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/matsola/" rel="noopener noreferrer"&gt;Volodymyr&lt;/a&gt;, for supporting me with the React Native implementation.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/matteoronchi/" rel="noopener noreferrer"&gt;Matteo&lt;/a&gt;, for trying the visual coverage with a different company.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/massimilianomantione/" rel="noopener noreferrer"&gt;Massimiliano&lt;/a&gt;, for the performance suggestions.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/blv-dmitry/" rel="noopener noreferrer"&gt;Dmitry&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/omri-lavi/" rel="noopener noreferrer"&gt;Omri&lt;/a&gt;, for the feedback.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://x.com/beaussan" rel="noopener noreferrer"&gt;Nicolas&lt;/a&gt;, for the frigging detailed review ❤️.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Would you like to join me and work in a purpose-driven organization where work, growth, and learning happen at the same time? Preply continues growing and we are actively looking for talented candidates to join our Engineering team! If you are excited about taking on a new challenge,&lt;/em&gt; &lt;a href="https://bit.ly/3Ei3FRt" rel="noopener noreferrer"&gt;&lt;em&gt;check out our open positions here&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>designsystem</category>
      <category>performance</category>
      <category>data</category>
    </item>
    <item>
      <title>Visual coverage: Why and How Preply Measures the Impact of the Design System (part I)</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Thu, 03 Oct 2024 11:26:46 +0000</pubDate>
      <link>https://forem.com/noriste/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-part-i-3omb</link>
      <guid>https://forem.com/noriste/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-part-i-3omb</guid>
      <description>&lt;p&gt;&lt;em&gt;This article has been originally posted &lt;a href="https://medium.com/preply-engineering/visual-coverage-why-and-how-preply-measures-the-impact-of-the-design-system-1057115f4aff" rel="noopener noreferrer"&gt;on Preply's engineering blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We implemented a custom way to measure the impact of the Design System, and we measure it on user’s devices. Why? Why were the existing solutions and approaches not enough? Why is it so crucial for us that everyone, management included, can understand and visualize the design system’s impact?&lt;/p&gt;

&lt;p&gt;We worked for months to come up with our own way to measure the design system impact, and we shared all the whys and the hows in this article. A longer, technical one (&lt;a href="https://dev.to/noriste/the-implementation-details-of-preplys-design-system-visual-coverage-part-ii-1ao2"&gt;The Implementation Details of Preply’s Design System Visual Coverage&lt;/a&gt;), explains what we did. Last but not least, the code is open source (&lt;a href="https://github.com/preply/design-system-visual-coverage" rel="noopener noreferrer"&gt;https://github.com/preply/design-system-visual-coverage&lt;/a&gt;).&lt;/p&gt;




&lt;p&gt;You may have heard of the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the &lt;a href="https://www.intodesignsystems.com/" rel="noopener noreferrer"&gt;Into Design Systems conference&lt;/a&gt;: here you can find &lt;a href="https://intodesignsystems.substack.com/p/measuring-design-system-impact-lessons" rel="noopener noreferrer"&gt;the article they wrote about it&lt;/a&gt;, and &lt;a href="https://www.figma.com/community/file/1509535015145685735/design-system-visual-coverage-resources" rel="noopener noreferrer"&gt;the resources we shared&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;At the &lt;a href="https://www.jsday.it/" rel="noopener noreferrer"&gt;JSDay&lt;/a&gt; conference: here is &lt;a href="https://www.jsday.it/talks_speakers/#DesignSystemVisualCoverageInWebAndApp(ReactNative)Applications" rel="noopener noreferrer"&gt;the talk description&lt;/a&gt;, and the &lt;a href="https://cef62.github.io/design-system-coverage-jsday-2025" rel="noopener noreferrer"&gt;slides&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;At one of &lt;a href="https://www.belkadigital.com/" rel="noopener noreferrer"&gt;Belka&lt;/a&gt;’s AMA: here is &lt;a href="https://www.youtube.com/watch?v=Kw_eGDppG4M" rel="noopener noreferrer"&gt;the recording&lt;/a&gt; (in Italian).&lt;/li&gt;
&lt;li&gt;At one of the &lt;a href="https://podcasts.apple.com/it/podcast/commit-to-growth/id1788326535" rel="noopener noreferrer"&gt;Commit to Growth&lt;/a&gt;’s episodes: here is &lt;a href="https://podcasts.apple.com/us/podcast/lessons-from-large-codebases-design-visual-coverage/id1788326535?i=1000688799413" rel="noopener noreferrer"&gt;the recording&lt;/a&gt;. The podcast is curated by one of Preply’s engineers: 
&lt;a href="https://medium.com/u/754df0a2b156?source=post_page---user_mention--1057115f4aff---------------------------------------" rel="noopener noreferrer"&gt;Yasemin çidem&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Why Did We Need a Custom Way to Track the Impact of the Design System?
&lt;/h1&gt;

&lt;p&gt;Preply is a data-driven company. All the teams elaborate on a long-term strategy (following &lt;a href="https://medium.com/mbreads/good-strategy-bad-strategy-b37b2563a4a3" rel="noopener noreferrer"&gt;the Good Strategy, Bad Strategy&lt;/a&gt; book approach) and define the metrics that allow them to measure their successes toward that strategy. The metrics are evaluated, challenged, discussed, tweaked, and approved. The design system team makes no exceptions.&lt;/p&gt;

&lt;p&gt;We found that none of the existing ways to track the impact of design systems could tell the actual influence Path (Preply’s Design System) has on Preply’s users. &lt;strong&gt;Explaining static analysis data (tokens and components used in the codebase) to the CEO is hard&lt;/strong&gt;. We needed to measure the impact on what the users see and interact with.&lt;/p&gt;

&lt;p&gt;Since 2021, static analysis has shown a positive trend regarding the design system’s adoption, reporting a boost during Preply’s 2023 re-brand. However, this data expresses the influence on the codebase, not the users.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2ACJri5mgDbKcWtaTf" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2ACJri5mgDbKcWtaTf" alt="A graph showing the growing adoption of the design system in the Preply's front-end projects. At the end of 2023, it shows that 25% of the front-end codebases come from the design system." width="1600" height="709"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A screenshot of the internal design system stats we collected. The labels at the top left are the internal project names.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What Have We Implemented? The Design System Visual Coverage
&lt;/h1&gt;

&lt;p&gt;The idea is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The borders of the DOM elements are colored green for those generated by Path design system components and red for the others.&lt;/li&gt;
&lt;li&gt; The borders’ size depends on the importance of the components.&lt;/li&gt;
&lt;li&gt; The formula is green pixels / (green pixels + red pixels) (the final formula slightly changed, but this is the core concept). So, &lt;strong&gt;100% means that “all the DOM elements of a page come from Path Design System components.”&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a screenshot of the visual coverage on Preply.com’s home page.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2A-WcFFM9M0KrWTf8Q" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2A-WcFFM9M0KrWTf8Q" alt="The Preply.com page showing the design system visual coverage in action: every DOM element has a colored border." width="1400" height="776"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Here, you can see the visual coverage in action with borders colored and weighted based on the type of components.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The visual coverage tracks how much the Path Design System components are used in Preply’s products.&lt;/p&gt;

&lt;h1&gt;
  
  
  How Is the Visual Coverage Used?
&lt;/h1&gt;

&lt;p&gt;The visual coverage serves different purposes:&lt;/p&gt;

&lt;p&gt;The Design System team can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Constantly track the outcomes of its initiatives. Please note that visual coverage is not the only metric we use.&lt;/li&gt;
&lt;li&gt; Identify where Path Design System components are used the most and the least, and work with Product teams to increase the visual coverage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The product teams have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Numbers they can use as OKRs.&lt;/li&gt;
&lt;li&gt; Straightforward tools to gather the visual coverage in seconds straight where they work: in production, locally, or in Storybook.&lt;/li&gt;
&lt;li&gt; Straightforward tools to split the ownership of the pages across different teams. Most of Preply.com’s pages are made of components that belong to various teams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To serve the mentioned purposes, we built an internal dashboard.&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%2Fjqynr06q1aynhvevuvwq.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%2Fjqynr06q1aynhvevuvwq.png" alt="The dashboard shows the current visual coverage, the average per team, the trends, etc." width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Our dashboard with teams, pages, and overall visual coverage.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Teams’ Ownership and Coverage Containers
&lt;/h2&gt;

&lt;p&gt;On Preply.com, some pages are owned by a single team, whereas different teams own others. Visual coverage containers are areas of the page that are analyzed and counted independently.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2ADj_AeIWUnRfDQKE_" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2ADj_AeIWUnRfDQKE_" alt="The screenshot shows how a single page can be split in different containers owned by different teams." width="1600" height="1000"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The coverage containers in action: in the screenshot, there are three different containers: the Header, the Calendar, and the rest of the page. A different Preply team owns each of them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While speaking with teams, we have always stressed that &lt;strong&gt;there is no good or bad visual coverage&lt;/strong&gt;. In Preply, Path Design System is heavily used, and when it’s not used very much, there are strong reasons. For instance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Calendars: Only one team uses calendars, and Path Design System does not offer pre-made calendar components. That team has low coverage compared to the others, and that’s fine!&lt;/li&gt;
&lt;li&gt; A payment button that comes from a third-party integration. It looks like a Preply button, but it is not.&lt;/li&gt;
&lt;li&gt; Every custom component has unique needs that the design system components do not offer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the same time, all the newly implemented product parts or the frequently refactored parts have the highest score, which has grown organically.&lt;/p&gt;

&lt;p&gt;This topic is essential because we never ask teams to reach a specific visual coverage score. &lt;strong&gt;Visual coverage is a litmus&lt;/strong&gt; test for many other factors heavily influenced by the Design System team and initiatives, not a goal to achieve at any cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Run the Visual Coverage Count? On Users’ Devices!
&lt;/h2&gt;

&lt;p&gt;The implementation of visual coverage (we shared an “implementation details” article &lt;a href="https://dev.to/noriste/the-implementation-details-of-preplys-design-system-visual-coverage-part-ii-1ao2"&gt;here&lt;/a&gt;) ensures that it doesn’t affect the user’s UX. That’s why we can track visual coverage in production on users’ devices. This choice made everything more complex from a technical perspective (we also a/b tested it to ensure UX was not affected). Still, it also made the design system team independent in implementing, deploying, evolving, and gathering thousands of data daily.&lt;/p&gt;

&lt;p&gt;If we decided to embed the visual coverage inside Preply’s E2E tests, we would have needed product teams to help us. However, product teams have product priorities, and depending on them would have meant delaying the project.&lt;/p&gt;

&lt;p&gt;Some considerations for measuring the visual coverage on users’ devices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Users use different devices with varying &lt;strong&gt;screen ratios and sizes&lt;/strong&gt;, making using a fixed device as a reference impossible.&lt;/li&gt;
&lt;li&gt; Users see different things based on the &lt;strong&gt;user-generated content&lt;/strong&gt; and custom interactions with the Product.&lt;/li&gt;
&lt;li&gt; The more a page is visited, the more calculations it sends. The final visual coverage score is an average of all the calculations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a result, the visual coverage is not 100% stable and can vary daily for the same page. We embraced this variability because the coverage would reflect what our users see!&lt;/p&gt;

&lt;h2&gt;
  
  
  Pages’ popularity impact
&lt;/h2&gt;

&lt;p&gt;Again, the more a page is visited, the more calculations it sends. I want to stress this topic because it’s crucial to visual coverage. When we count the visual coverage, we send two pieces of data to the server: the number of pixels of the Path Design System components’ perimeters and the number of pixels of the other DOM elements. In DataDog (used for our dashboard), we calculate the average.&lt;/p&gt;

&lt;p&gt;Imagine this scenario (numbers are imaginary):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Home page is visited 5 times&lt;/strong&gt;, and it sends the following events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; One event for the Design System pixels count. Let’s imagine the value is 10K&lt;/li&gt;
&lt;li&gt; One event Non-Design System pixels: 5K&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In total, the Home page sent 10 events. The Home page’s visual coverage is 67%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setting page is visited 1 times&lt;/strong&gt;, and it sends the following events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; One event for the Design System pixels count. Let’s imagine the value is 1K&lt;/li&gt;
&lt;li&gt; One event Non-Design System pixels: 20K&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In total, the Settings page sent 4 events. The Settings page’s visual coverage is 4%.&lt;/p&gt;

&lt;p&gt;The math done in the dashboard is: all the Design System pixels count / divided by all the pixel counts. (10K*5 + 1K*2) / (10K*5 + 1K*2 + 5K*5 + 20K*2) = 44%. Can you see how &lt;strong&gt;the high popularity of the Home page weighs more&lt;/strong&gt; than the low popularity of the Settings page? The final 44% is closer to the Home page’s 67% than the Settings page’s 4%.&lt;/p&gt;

&lt;p&gt;Now, consider that our public and indexed pages are visited millions of times, and other “private” pages are visited hundreds of thousands of times. Other B2B pages could have “just” tens of thousands of visits. This is crucial information because, when we do the math, the teams behind the B2B pages &lt;strong&gt;could be entirely obfuscated by the popularity of other teams’ public pages&lt;/strong&gt;. That’s why the Design System team cares more about the average per team than the final number (at the time of writing, our visual coverage is 77%, but most of the teams are above 85%) because the per-team visual coverage allows more granular monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Components’ Weights
&lt;/h2&gt;

&lt;p&gt;What counts for the visual coverage are the component’s perimeters. The perimeters can’t be the same for all the components for some reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Path Design System components are UI components, while the &lt;strong&gt;pages contain hundreds of layout containers&lt;/strong&gt;, which don’t impact the UX. UI components are smaller than containers, so the latter weigh more if the weight is the same.&lt;/li&gt;
&lt;li&gt; A Design System’s divider doesn’t impact the UX as much as a searchable dropdown. At the same time, a Dropdown &lt;strong&gt;saves developers weeks of work&lt;/strong&gt; while a Divider saves seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, the component weights are crucial for the visual coverage. We had a workshop to define the component’s importance. The idea is&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; To identify a 0–5 score for the component’s impact in terms of &lt;strong&gt;UI, UX, and accessibility&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; We did it empirically to identify the importance of every factor: every team member voted and gave a 1–3 score to every factor; the importance is the sum of the votes.&lt;/li&gt;
&lt;li&gt; Then, multiply the scores we identified at point 1 for the importance identified at point 2.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The result is a relative component’s score&lt;/strong&gt;. In our case, it goes from 0 to 60.&lt;/li&gt;
&lt;/ol&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AG8X1bKdhp7AcanS1" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A2000%2Fformat%3Awebp%2F0%2AG8X1bKdhp7AcanS1" alt="The spreadsheet with all the components and all the scores." width="1600" height="1288"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The spreadsheet we used to identify the weight to assign to every component.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The actual weight we use for the visual coverage is reduced by a factor we identified through multiple iterations. For each iteration, we analyzed the score of the most representative pages (some of them are the most important ones, and some have low Path Design System’s components usage), &lt;strong&gt;empirically looking for the right coverage for the pages&lt;/strong&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Other Design System Metrics
&lt;/h1&gt;

&lt;p&gt;While elaborating on a long-term Design System strategy and a yearly roadmap, we thought of which meaningful metrics we could use to track the Path Design System’s success. The visual coverage seems to group all of them (because an increase in the visual coverage is the litmus of many other initiatives). Still, at the same time, it can’t reflect everything the design system team does. The metrics we care about are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The Design System &lt;strong&gt;NPS score&lt;/strong&gt;, of course.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Designers’ and developers’ efficiency&lt;/strong&gt;: initially, this is a soft metric we ask through surveys without a real process in place to calculate it&lt;/li&gt;
&lt;li&gt; The percentage of Path Design System &lt;strong&gt;foundations and components documented&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Path Design System &lt;strong&gt;platform features parity&lt;/strong&gt;: the three platforms are Figma, Web, and App. Path Design System should support all of them at the same level (considering some unique capabilities and needs. For instance, Figma does not require Path’s components to be SSR/RSC-friendly).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And, more in general, we monitor&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Path’s components usage: static analysis is insightful for the design system team.&lt;/li&gt;
&lt;li&gt; Figma’s components detachments through Figma Analytics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also discarded implementing some metrics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Figma adoption&lt;/strong&gt;: We differentiate coverage (usage) from adoption (&lt;strong&gt;correct&lt;/strong&gt; usage), but after some experiments and chats with other Design System teams, it seems too early for us to work on it, given the time required to implement and maintain something reliable.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The amount of CSS introduced per PR:&lt;/strong&gt; This is one of the metrics Cristiano Rastelli identified in his &lt;a href="https://didoo.medium.com/measuring-the-impact-of-a-design-system-7f925af090f7" rel="noopener noreferrer"&gt;Measuring the Impact of a Design System&lt;/a&gt;, which is the best resource on this topic. We suggest reading the article to find links to other companies’ activities to track their Design System impact.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Implementation details
&lt;/h1&gt;

&lt;p&gt;Implementing the visual coverage, collecting feedback, improving it, validating it through a company-wide visual coverage increase initiative, etc., required months of work. Other companies we discussed showed interest, too.&lt;/p&gt;

&lt;p&gt;If you want to experiment with it, we've got you covered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Read (or send it to your engineers) &lt;a href="https://dev.to/noriste/the-implementation-details-of-preplys-design-system-visual-coverage-part-ii-1ao2"&gt;the second part of this article&lt;/a&gt;, which is dedicated to the implementation details.&lt;/li&gt;
&lt;li&gt; Fork, tweak, and use &lt;a href="https://github.com/preply/design-system-visual-coverage" rel="noopener noreferrer"&gt;the visual coverage code&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let us know what you think, and contact us at design-system [AT] preply.com, or directly with me on &lt;a href="https://x.com/NoriSte" rel="noopener noreferrer"&gt;X&lt;/a&gt;, &lt;a href="https://github.com/NoriSte" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, or &lt;a href="https://www.linkedin.com/in/noriste/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Resources
&lt;/h1&gt;

&lt;p&gt;Preply looks very different from September 2023, and Path Design System is now quite connected to the new brand. If you are curious about how Preply executed a company-wide revolution in a few months, please read &lt;a href="https://www.linkedin.com/in/maryna-shananina-a8535a82/" rel="noopener noreferrer"&gt;Maryna&lt;/a&gt;’s dedicated article, &lt;a href="https://medium.com/preply-engineering/turning-the-page-preplys-successful-journey-through-the-rebrand-e1ebf2a4612f" rel="noopener noreferrer"&gt;Turning the Page: Preply’s Successful Journey Through the Rebrand&lt;/a&gt;).&lt;/p&gt;

&lt;h1&gt;
  
  
  Special Thanks
&lt;/h1&gt;

&lt;p&gt;I want to publicly kudos all the people involved in this project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The whole design system team: &lt;a href="http://justine" rel="noopener noreferrer"&gt;Justine&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/seifkamal/" rel="noopener noreferrer"&gt;Seif&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/lightespresso/" rel="noopener noreferrer"&gt;Alex&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/javierarques/" rel="noopener noreferrer"&gt;Javi&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/alisa-antypova/" rel="noopener noreferrer"&gt;Alisa&lt;/a&gt;. I love this team 😍&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/vadym-vlasenko-699a9222/" rel="noopener noreferrer"&gt;Vadym&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/ihor-kasianov-b58b783a/" rel="noopener noreferrer"&gt;Igor&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/joshcrossick/" rel="noopener noreferrer"&gt;Josh&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/dmytrovoloshyn/" rel="noopener noreferrer"&gt;Dmytro&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/dominika-szuka%C5%82a-kosi%C5%84ska-1240627a/" rel="noopener noreferrer"&gt;Dominika&lt;/a&gt;, for the support and feedback 😊&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/oleg-buiar/" rel="noopener noreferrer"&gt;Oleg&lt;/a&gt;, for creating the dashboard and helping with all the data.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/daniel-guerra-guerrero-a64180a9/" rel="noopener noreferrer"&gt;Daniel&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/albertovilva" rel="noopener noreferrer"&gt;Alberto&lt;/a&gt;, for the early internal feedback.&lt;/li&gt;
&lt;li&gt; All the Preply front-end engineers for supporting the design system, and this initiative. All the Preply app engineers for helping with the React Native implementation.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/jalvarez88/" rel="noopener noreferrer"&gt;Javier&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/serhii-tanskyi-243779143/" rel="noopener noreferrer"&gt;Serhii&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/matsola/" rel="noopener noreferrer"&gt;Volodymyr&lt;/a&gt;, for supporting me with the React Native implementation.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/matteoronchi/" rel="noopener noreferrer"&gt;Matteo&lt;/a&gt;, for trying the visual coverage with a different company.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/massimilianomantione/" rel="noopener noreferrer"&gt;Massimiliano&lt;/a&gt;, for the performance suggestions.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://www.linkedin.com/in/blv-dmitry/" rel="noopener noreferrer"&gt;Dmitry&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/omri-lavi/" rel="noopener noreferrer"&gt;Omri&lt;/a&gt;, for the feedback.&lt;/li&gt;
&lt;li&gt; &lt;a href="https://x.com/beaussan" rel="noopener noreferrer"&gt;Nicolas&lt;/a&gt;, for the frigging detailed review ❤️.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Would you like to join me and work in a purpose-driven organization where work, growth, and learning happen at the same time? Preply continues growing and we are actively looking for talented candidates to join our Engineering team! If you are excited about taking on a new challenge,&lt;/em&gt; &lt;a href="https://bit.ly/3Ei3FRt" rel="noopener noreferrer"&gt;&lt;em&gt;check out our open positions here&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>designsystem</category>
      <category>performance</category>
      <category>data</category>
    </item>
    <item>
      <title>Production-grade gradient bordered, transparent, and rounded button</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Thu, 21 Sep 2023 15:29:44 +0000</pubDate>
      <link>https://forem.com/noriste/production-grade-gradient-bordered-transparent-and-rounded-button-56h4</link>
      <guid>https://forem.com/noriste/production-grade-gradient-bordered-transparent-and-rounded-button-56h4</guid>
      <description>&lt;p&gt;There are a lot of articles out there on making gradient-bordered buttons, but the ones I read do not provide a &lt;strong&gt;production-grade solution&lt;/strong&gt;. At &lt;a href="https://preply.com/" rel="noopener noreferrer"&gt;Preply&lt;/a&gt;, we needed to have a partially transparent background button with rounded corners, and with gradient border. We need to get it in our Design System, to think about a React Native solution. I know this article is long, but it includes the links to all the resources I read, the solutions I discarded, a link to the Figma file containing the gradient border, and &lt;a href="https://codepen.io/NoriSte/pen/LYMePqQ" rel="noopener noreferrer"&gt;a Codepen where you can play with it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you follow me: this article is quite different compared to the high-level, senior-oriented articles I usually write (here is &lt;a href="https://github.com/NoriSte/all-my-contributions#articles" rel="noopener noreferrer"&gt;the full list of them&lt;/a&gt;) 🙏 but I needed to write it down to be sure it helps other developers in the future.&lt;/p&gt;




&lt;p&gt;I had to implement a new variant of the Preply button for our internal Design System: a semi-transparent button with a gradient-rounded border.&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%2F3mx9t90scv6qvqv5stsh.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%2F3mx9t90scv6qvqv5stsh.png" alt="The transparent button with gradient borders" width="668" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The existing solutions
&lt;/h2&gt;

&lt;p&gt;There are some solutions out there but we needed to discard most of them because they do not match the following requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The button &lt;strong&gt;must be transparent&lt;/strong&gt;: this prevents us from using the most common solution presented in the &lt;a href="https://medium.com/@christianjbolus/how-to-make-a-button-with-a-gradient-border-and-gradient-text-in-html-css-7d495656169" rel="noopener noreferrer"&gt;How To Make a Button with a Gradient Border and Gradient Text in HTML &amp;amp; CSS&lt;/a&gt; post. The solution presented here relies on two elements, with a pseudo-element that has the same background color as the page background covering the real element that has a gradient background.&lt;/li&gt;
&lt;li&gt;The button must be &lt;strong&gt;half transparent&lt;/strong&gt;: the solution presented on &lt;a href="https://stackoverflow.com/questions/51868069/button-gradient-borders-with-transparent-background" rel="noopener noreferrer"&gt;StackOverflow's Button gradient borders with transparent background&lt;/a&gt; does not work since it gives a false idea of transparency and fits just one possible background.&lt;/li&gt;
&lt;li&gt;It must have &lt;strong&gt;wider browser support&lt;/strong&gt;: using engine-only CSS properties like &lt;code&gt;-webkit-mask&lt;/code&gt; as suggested in &lt;a href="https://dev.to/afif/border-with-gradient-and-radius-387f"&gt;Border with gradient and radius&lt;/a&gt; does not work well for us.&lt;/li&gt;
&lt;li&gt;It must have &lt;strong&gt;rounded corners&lt;/strong&gt;: this discards using CSS &lt;code&gt;border-image&lt;/code&gt; (here is a &lt;a href="https://css-tricks.com/almanac/properties/b/border-image/" rel="noopener noreferrer"&gt;good CSS Tricks article&lt;/a&gt; about it) since it's not compatible with &lt;code&gt;border-radius&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;More:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it should have a fallback for the browsers that do not support the gradient border.&lt;/li&gt;
&lt;li&gt;the border should not be a raster image to avoid pixelation.&lt;/li&gt;
&lt;li&gt;It should be supported by React Native too: more on this topic will follow, forget about it for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The viable solution
&lt;/h2&gt;

&lt;p&gt;The last mentioned one, &lt;code&gt;border-image&lt;/code&gt;, in reality, meets all the requirements if mixed with a couple of tricks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We can use an &lt;strong&gt;SVG instead or a raster image&lt;/strong&gt;: pixelation is not a problem anymore.&lt;/li&gt;
&lt;li&gt;It's widely supported: at the time of writing, &lt;a href="https://caniuse.com/border-image" rel="noopener noreferrer"&gt;Can I use reports 96.62%&lt;/a&gt; (95.43% if unprefixed) browser usage compatibility.&lt;/li&gt;
&lt;/ol&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%2Fvven6fabllvzxotog7sr.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%2Fvven6fabllvzxotog7sr.png" alt="Can I use reporting the current browser support" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Through &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-slice" rel="noopener noreferrer"&gt;&lt;code&gt;border-image-slice: stretch&lt;/code&gt;&lt;/a&gt; we can define the portions that should scale when the button gets bigger (the sides) and the ones that should not (the corners).&lt;/li&gt;
&lt;li&gt;We can use an SVG that includes rounded corners to simulate the final rounded corners.&lt;/li&gt;
&lt;li&gt;We can work around the few bugs &lt;a href="https://caniuse.com/border-image" rel="noopener noreferrer"&gt;reported by Can I use&lt;/a&gt; easily (we simply need to use the shorthand syntax) and they do not impact &lt;code&gt;border-image-slice: stretch&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step-by-step implementation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Creating the SVG in Figma. This is &lt;a href="https://www.figma.com/file/PVFEcmnxageYu3Lawp0wLU/gradient-button?type=design&amp;amp;node-id=0%3A1&amp;amp;mode=design&amp;amp;t=RsDQDycj2TUR3oCF-1" rel="noopener noreferrer"&gt;the Figma file containing it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Essentially, it's a 100x100 rounded square with the desired border. I made it 100x100 to avoid any kind of coordinates-related problem mentioned in &lt;a href="https://css-tricks.com/almanac/properties/b/border-image/#aa-border-image-slice" rel="noopener noreferrer"&gt;the CSS Tricks article&lt;/a&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%2F5k7l4od64w6fxmdwk5q8.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%2F5k7l4od64w6fxmdwk5q8.png" alt="The gradient square" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Optimize the SVG. I was looking for the best way to embed the SVG (external or embedded image?) and the CSS Tricks article points to the great &lt;a href="https://css-tricks.com/probably-dont-base64-svg/" rel="noopener noreferrer"&gt;Probably Don’t Base64 SVG&lt;/a&gt; that, in  turn, points to &lt;a href="https://codepen.io/tigt/post/optimizing-svgs-in-data-uris" rel="noopener noreferrer"&gt;Optimizing SVGs in data URIs&lt;/a&gt;. You can leverage the findings from the latter through &lt;a href="https://www.npmjs.com/package/mini-svg-data-uri" rel="noopener noreferrer"&gt;mini-svg-data-uri&lt;/a&gt;. The steps are&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reducing the SVG size: easily doable through &lt;a href="https://jakearchibald.github.io/svgomg/" rel="noopener noreferrer"&gt;SVGOMG&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Passing the result through mini-svg-data-uri: &lt;code&gt;$ npx mini-svg-data-uri image.svg image.svg.uri&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy-pasting the &lt;code&gt;image.svg.uri&lt;/code&gt; content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;border-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none'%3e%3cpath stroke='url(%23a)' stroke-width='2' d='M8 1h84a7 7 0 0 1 7 7v84a7 7 0 0 1-7 7H8a7 7 0 0 1-7-7V8a7 7 0 0 1 7-7Z'/%3e%3cdefs%3e%3clinearGradient id='a' x1='20.27' x2='120.715' y1='125' y2='113.589' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='%23FF7AAC'/%3e%3cstop offset='.367' stop-color='%23FF7AAC'/%3e%3cstop offset='.682' stop-color='%232885FD'/%3e%3cstop offset='1' stop-color='%233DDABE'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choosing the inline or hosted one leads to a different UX, obviously. When I was playing with the hosted one, I hated that the border came with a little delay because of the time required to load the SVG remotely...&lt;/p&gt;

&lt;p&gt;Please note that using &lt;code&gt;mini-svg-data-uri&lt;/code&gt; is mandatory, otherwise, the browser does not recognize the SVG. Alternatively, this problem does not happen if you convert the SVG to Base64.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Choosing the right &lt;code&gt;border-image-slice&lt;/code&gt; and &lt;code&gt;border-image-width&lt;/code&gt; values. If you need to do something more complex than the case reported in this guide, please refer to the official documentation (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-slice" rel="noopener noreferrer"&gt;1&lt;/a&gt;, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-width" rel="noopener noreferrer"&gt;2&lt;/a&gt;) and again to &lt;a href="https://css-tricks.com/almanac/properties/b/border-image/#aa-border-image-slice" rel="noopener noreferrer"&gt;the great CSS Tricks article&lt;/a&gt;. But for the sake of this guide, the value for either of them is just &lt;code&gt;8&lt;/code&gt;. Why? Because &lt;code&gt;8&lt;/code&gt; is the border radius of the button.&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%2Fxhy7htfideri5j0dy69h.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%2Fxhy7htfideri5j0dy69h.png" alt="Figma showing that the border radius is 8" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you imagine the border width acting as a sort of "mask" (I highlighted it in yellow in the next images) it must be &lt;code&gt;8&lt;/code&gt; to be sure it includes the rounder corner.  If, instead you set &lt;code&gt;2&lt;/code&gt; as the real border width, the result is that the "mask" cuts out the rounded corners.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;8px&lt;/th&gt;
&lt;th&gt;2px&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fg4ew4mpnm3sjzjg284jh.png" alt="A simulation of a 8px mask on the svg" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&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%2Fv9wovfcmx7tnhrscjrvo.png" alt="A simulation of a 2px mask on the svg" width="800" height="800"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;here you can see the final result of using &lt;code&gt;8&lt;/code&gt; (correct) and &lt;code&gt;2&lt;/code&gt; (incorrect) as rendered by the browser&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%2Faoyeqdmxtwttq1x6hke7.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%2Faoyeqdmxtwttq1x6hke7.png" alt="How the browser renders the 8px and the 2px ones" width="596" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, the code is now&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;border-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"data:image/svg+xml,..."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;p.s. if you set the &lt;code&gt;border-width&lt;/code&gt; to &lt;code&gt;8&lt;/code&gt; instead of &lt;code&gt;border-image-width&lt;/code&gt;, you obtain the same visual effect... but the border is effectively 8 pixels, not 2 pixels like I set for the button! Keep it in mind if there is something wrong and you do not understand why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;: Choosing the correct &lt;code&gt;border-image-repeat&lt;/code&gt; property. It's &lt;code&gt;stretch&lt;/code&gt;, as I told you earlier. Essentially, it means "Don't touch the corners but extend the sides to be sure the image covers all the size of the button".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;border-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"data:image/svg+xml,..."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;stretch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;: Enjoy the full code 😊&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;border-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none'%3e%3cpath stroke='url(%23a)' stroke-width='2' d='M8 1h84a7 7 0 0 1 7 7v84a7 7 0 0 1-7 7H8a7 7 0 0 1-7-7V8a7 7 0 0 1 7-7Z'/%3e%3cdefs%3e%3clinearGradient id='a' x1='20.27' x2='120.715' y1='125' y2='113.589' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='%23FF7AAC'/%3e%3cstop offset='.367' stop-color='%23FF7AAC'/%3e%3cstop offset='.682' stop-color='%232885FD'/%3e%3cstop offset='1' stop-color='%233DDABE'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;stretch&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Browser compatibility
&lt;/h2&gt;

&lt;p&gt;At Preply, we must support a wide range of browsers. By looking at the &lt;a href="https://caniuse.com/border-image" rel="noopener noreferrer"&gt;Can I use&lt;/a&gt; table I shared above, it's hard to find a non-supported browser and I can be sure the gradient works everywhere. Anyway, I wanted to double-check it with my eyes and I initially used &lt;a href="https://www.browserstack.com/screenshots" rel="noopener noreferrer"&gt;BrowserStack screenshots&lt;/a&gt; to test out the solution quickly on a lot of browsers, but it resulted in a bit of unreliable since it was not able to render most of the screenshots due to internal BrowserStack problems... Anyway, I'm happy because when it was able to render the normal button, it was also able to render the gradient one. Here are some of the screenshots on some "not-so-common" browsers&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Samsung Galaxy S20&lt;/th&gt;
&lt;th&gt;MacOS / Safari 13&lt;/th&gt;
&lt;th&gt;Windows 10 / Edge 18&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fo3sijabwajm5jlf35jwu.jpg" alt="Samsung Galaxy S20 screenshot" width="360" height="876"&gt;&lt;/td&gt;
&lt;td&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%2Fowciykp00o94zqwk47is.png" alt="MacOS / Safari 13 screenshot" width="800" height="582"&gt;&lt;/td&gt;
&lt;td&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%2F6ns45tg2rr4zmtb9xvn6.png" alt="Windows 10 / Edge 18 screenshot" width="800" height="542"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fallback
&lt;/h2&gt;

&lt;p&gt;There are for sure some browsers out there that do not support &lt;code&gt;border-image&lt;/code&gt;. There are two options here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using CSS Feature Queries (&lt;code&gt;@supports&lt;/code&gt;)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;border-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("data:image/svg+xml,...")&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="n"&gt;stretch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nl"&gt;border-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("data:image/svg+xml,...")&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="n"&gt;stretch&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;This is the solution that gives you more freedom since you can literally change all the button properties you want outside and inside the &lt;code&gt;@supports&lt;/code&gt; block, you are not limited to changing just &lt;code&gt;border-image&lt;/code&gt;. Please check the &lt;a href="https://caniuse.com/css-featurequeries" rel="noopener noreferrer"&gt;Feature Queries browser compatibility&lt;/a&gt;, even if they are almost the same as the one of &lt;code&gt;border-image&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simply choose a fallback &lt;code&gt;border-color&lt;/code&gt;. &lt;code&gt;border-image&lt;/code&gt; overwrites the &lt;code&gt;border-color&lt;/code&gt;, but the browsers not able to interpret &lt;code&gt;border-image&lt;/code&gt; will use &lt;code&gt;border-color&lt;/code&gt;. Here is an example of Internet Explorer 10.&lt;/li&gt;
&lt;/ol&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%2Fssffpssl0nx2wihmgfg9.jpg" 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%2Fssffpssl0nx2wihmgfg9.jpg" alt="Internet Explorer 10 screenshot" width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I opted for the second option because the LESS + PostCSS transpilation process breaks the &lt;code&gt;@supports&lt;/code&gt; property and I have not found a solution yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;border-image&lt;/code&gt; does not work with React Native (which simply renders the normal border color instead) and the app developers pointed me to some different solutions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the button has a gradient border but does not have a transparent background: they would use a combination of &lt;a href="https://github.com/react-native-linear-gradient/react-native-linear-gradient" rel="noopener noreferrer"&gt;react-native-linear-gradient&lt;/a&gt; with an element with an opaque background over it.&lt;/li&gt;
&lt;li&gt;If the button must also be transparent, also &lt;a href="https://www.npmjs.com/package/@react-native-masked-view/masked-view" rel="noopener noreferrer"&gt;masked-view&lt;/a&gt; would be needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I reported both cases because adding two dependencies for just one button could be overkill. When the button is added to the app, the app developers will decide what to do based on where the button is used. They will maybe opt for implementing the button in the app codebase instead of the Design System one, to avoid putting too many dependencies on the Design System codebase.&lt;/p&gt;

&lt;p&gt;But the button is not used in the app at the time of writing so we have the time to go back to the designers asking what they prefer to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional notes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't forget about &lt;code&gt;background: transparent;&lt;/code&gt; to make the button transparent, of course 😊.&lt;/li&gt;
&lt;li&gt;Our Design System System's button already sets it but if the button has a background that's not totally transparent, remember to set the &lt;code&gt;border-radius&lt;/code&gt; to &lt;code&gt;8px&lt;/code&gt; too to avoid the background coming out of the rounded borders.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Codepen liknk
&lt;/h2&gt;

&lt;p&gt;You can play with the final result in &lt;a href="https://codepen.io/NoriSte/pen/LYMePqQ" rel="noopener noreferrer"&gt;this Codepen&lt;/a&gt; 😊.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit where credit is due
&lt;/h2&gt;

&lt;p&gt;Thank you so much to all the creators sharing their solutions and tools! I would have spent maybe ten times the time on this without their explorations! ❤️&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Some things I learnt from working on big frontend codebases</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Thu, 01 Jun 2023 15:15:36 +0000</pubDate>
      <link>https://forem.com/noriste/some-things-i-learnt-from-working-on-big-frontend-codebases-1e0a</link>
      <guid>https://forem.com/noriste/some-things-i-learnt-from-working-on-big-frontend-codebases-1e0a</guid>
      <description>&lt;p&gt;Until now (May 2024), I had three experiences working on &lt;strong&gt;very big front-end (React+TypeScript) codebases&lt;/strong&gt;: &lt;a href="https://www.route-manager.com/" rel="noopener noreferrer"&gt;WorkWave RouteManager&lt;/a&gt;, &lt;a href="https://hasura.io/" rel="noopener noreferrer"&gt;Hasura&lt;/a&gt; Console, and &lt;a href="https://preply.com/" rel="noopener noreferrer"&gt;Preply.com&lt;/a&gt;. The former two are ~ 250K LOC, while Preply.com is close to 1M LOC, and the experiences are very different. In this article, I report the most important problems I saw while working on them, things that usually are not big deals if working on smaller codebases, but that become a source of big friction when the app scales.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@sandercrombach?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Sander Crombach&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/Zst8CE0bD9c?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Changelog&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;May, 2024

&lt;ul&gt;
&lt;li&gt;add Non-straightforward CI scripts
&lt;/li&gt;
&lt;li&gt;add Components accepting &lt;code&gt;className&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add Not tracking architectural decisions
&lt;/li&gt;
&lt;li&gt;add Spreading external dependencies and implementation details
&lt;/li&gt;
&lt;li&gt;add Hide stores implementation details
&lt;/li&gt;
&lt;li&gt;add Consuming Swiss army knifes
&lt;/li&gt;
&lt;li&gt;add Major product changes and refactors
&lt;/li&gt;
&lt;li&gt;update Never updating the NPM dependencies
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;June, 2023

&lt;ul&gt;
&lt;li&gt;first article publication&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  My direct experience
&lt;/h2&gt;

&lt;p&gt;First of all, let me describe the main characteristics of the two projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.route-manager.com/" rel="noopener noreferrer"&gt;WorkWave RouteManager&lt;/a&gt;: the product is very complex due to some back-end limitations that force the front-end to take care of a bigger complexity. Anyway, due to the strong presence of the great front-end Architect (that's &lt;a href="https://www.linkedin.com/in/matteoronchi/" rel="noopener noreferrer"&gt;Matteo Ronchi&lt;/a&gt;, by the way), &lt;strong&gt;the codebase can be considered front-end perfection&lt;/strong&gt;. The codebase is completely new (rewritten from scratch from 2020 to 2022), and trying and using the latest tools happened on a high cadence (for instance: we started using Recoil way sooner than the rest of the world, we &lt;a href="https://dev.to/noriste/migrating-a-150k-loc-codebase-to-vite-and-esbuild-why-part-1-3-2idj"&gt;migrated the codebase from Webpack to Vite in 2021&lt;/a&gt;, etc.), and the coding patterns are respected everywhere. The team was made by the four front-end engineers, including the architect and I. Here I was the team leader of the front-end team.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://hasura.io/" rel="noopener noreferrer"&gt;Hasura Console&lt;/a&gt;: the complexity of the project is not so high but the startup needs (pushing out new features as soon as possible) and the very back-end nature of the platform later resulted in huge technical debt and antipatterns that became into big friction points for the developers working on the front-end. The team was made of 12 front-end engineers, and &lt;strong&gt;later on the company decided to ditch the front-end project&lt;/strong&gt; creating a 50x smaller one, and keep only the back-end/CLI projects. Here, I joined as a senior front-end engineer and then I became the tech lead of the platform team.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://preply.com/" rel="noopener noreferrer"&gt;Preply.com&lt;/a&gt;: Preply scaled following a very experiment and data-driven approach, given its B2C nature and the million of users taking lessons on the platform every day. The natural business orientation lead to heavy outdated front-end dependencies and hard-to-work-with front-end projects. At the same time, Preply's goal to become a strong brand, to grow in the B2B market, and the tireless dedication to its internal culture and employees' satisfaction, drove the company to care a lot about internal tech excellence, to create a DevEx team inside the Platform team, and to &lt;strong&gt;lead some specimen tech initiatives&lt;/strong&gt;. The company counted ~40 frontend-end engineers, some of them dedicated to React Native. I joined the platform team as a senior front-end engineer and then I moved to the Design System team.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Following, is a non-exhaustive list of examples coming from some of the characteristics/activities/problems I saw, grouped by categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
Generic approaches

&lt;ul&gt;
&lt;li&gt;Managing more cases than the needed ones&lt;/li&gt;
&lt;li&gt;Leaving dead code around&lt;/li&gt;
&lt;li&gt;Internal code dependencies and boundaries&lt;/li&gt;
&lt;li&gt;Implicit dependencies&lt;/li&gt;
&lt;li&gt;Spreading external dependencies and implementation details&lt;/li&gt;
&lt;li&gt;Big modules&lt;/li&gt;
&lt;li&gt;Code readability&lt;/li&gt;
&lt;li&gt;Uniformity is better than perfection&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Working flow

&lt;ul&gt;
&lt;li&gt;Not tracking architectural decisions&lt;/li&gt;
&lt;li&gt;No PR description and big PRs&lt;/li&gt;
&lt;li&gt;Suggesting big changes and approaches during code reviews&lt;/li&gt;
&lt;li&gt;When to fix the technical debt?&lt;/li&gt;
&lt;li&gt;Major product changes and refactors&lt;/li&gt;
&lt;li&gt;No front-end oriented back-end APIs&lt;/li&gt;
&lt;li&gt;Never updating the NPM dependencies&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

TypeScript

&lt;ul&gt;
&lt;li&gt;Bad practice: Generic TypeScript types and optional properties&lt;/li&gt;
&lt;li&gt;Type assertions (&lt;code&gt;as&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@ts-ignore&lt;/code&gt; instead of &lt;code&gt;@ts-expect-error&lt;/code&gt; and broad scope&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;any&lt;/code&gt; instead of &lt;code&gt;unknown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ESLint rules kept as warnings&lt;/li&gt;
&lt;li&gt;Validating the external data&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

React

&lt;ul&gt;
&lt;li&gt;HTML templating instead of clear JSX&lt;/li&gt;
&lt;li&gt;Lot of React hooks and logic in the component's code&lt;/li&gt;
&lt;li&gt;Components accepting &lt;code&gt;className&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hide stores implementation details&lt;/li&gt;
&lt;li&gt;Consuming Swiss army knifes&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Tests

&lt;ul&gt;
&lt;li&gt;Bad tests&lt;/li&gt;
&lt;li&gt;E2E tests everywhere&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Developer Experience

&lt;ul&gt;
&lt;li&gt;Deprecated APIs&lt;/li&gt;
&lt;li&gt;Care about the browser logs&lt;/li&gt;
&lt;li&gt;Developer alerts for unexpected things&lt;/li&gt;
&lt;li&gt;React-only APIs&lt;/li&gt;
&lt;li&gt;Non-straightforward CI scripts&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Generic approaches
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Managing more cases than the needed ones
&lt;/h4&gt;

&lt;p&gt;This innocent approach leads to big problems and a waste of time when you have to refactor a lot of code trying to maintain the existing features. Some examples are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Components/functions &lt;strong&gt;with optional props/parameters and fallback default values&lt;/strong&gt;: when you need to refactor the components you need to understand what are the indirect consumers of the default values... But what happens if the usage of the default values is driven by network responses? You need to understand and simulate all the edge cases! And what happens if you find out that the default values are not used at all? I once saw a colleague of mine wasting four hours during a refactor for an unused default value...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Types that are typed as a generic &lt;code&gt;string&lt;/code&gt; or generic &lt;code&gt;record&amp;lt;string, any&amp;gt;&lt;/code&gt; when in reality the possible values are known in advance. The result is a lot of code that manages generic strings and objects while managing the real finite amount of cases would be 10x easier. Again, when you need to refactor the code managing "generic" values, you are going to waste time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I touched on these topics in my &lt;a href="https://dev.to/noriste/how-i-ease-the-next-developer-reading-my-code-1986"&gt;How I ease the next developer reading my code&lt;/a&gt; article.&lt;/p&gt;

&lt;h4&gt;
  
  
  Leaving dead code around
&lt;/h4&gt;

&lt;p&gt;You refactor a module, you remove an import of an external module and you are fine. What happens if &lt;strong&gt;the module was the last consumer of the external one&lt;/strong&gt;? The external module becomes dead code that will not be embedded in the application (nice) but that will confuse everyone that's going around the codebase looking for solutions/utilities/patterns and will confuse the future refactorer that will blame anyone that left the unused module there!&lt;/p&gt;

&lt;p&gt;And obviously, it's a waterfall... the external module could import other unused modules and they could depend on an external NPM dependency that could be removed from the package.json, etc.&lt;/p&gt;

&lt;h4&gt;
  
  
  Internal code dependencies and boundaries
&lt;/h4&gt;

&lt;p&gt;Not enforcing (through ESLint rules or through a proper monorepo structure) &lt;strong&gt;strong boundaries among product features/libraries/utilities&lt;/strong&gt; bring unexpected breaks as a result of innocent changes. Something like FeatureA imports from internal modules of FeatureB that imports from internal modules FeatureA and FeatureC, etc. brings you to break 50% of the product by changing a simple prop in a FeatureA's component. And if you have a lot of JavaScript modules never converted to TypeScript, you will also have a hard time understanding the dependency tree among features...&lt;/p&gt;

&lt;p&gt;I strongly suggest reading &lt;a href="https://www.developerway.com/posts/react-project-structure" rel="noopener noreferrer"&gt;React project structure for scale: decomposition, layers and hierarchy&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Implicit dependencies
&lt;/h4&gt;

&lt;p&gt;They are the hardest things to deal with. Some examples?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global styles that impact your UI's look&amp;amp;feel in unexpected ways&lt;/li&gt;
&lt;li&gt;A global listener on some HTML attributes that does things without the developer knowing about them&lt;/li&gt;
&lt;li&gt;A generic MSW mock server that all the tests used but it's impossible to know what handlers are used by what tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, poor the refactorer that will deal with those. Instead, explicit imports, speaking HTML attributes, inversion of control, etc. allow you to easily recognize who consumes what.&lt;/p&gt;

&lt;h4&gt;
  
  
  Spreading external dependencies and implementation details
&lt;/h4&gt;

&lt;p&gt;External dependencies should be hidden and consumed by controlled code if you write a custom function called &lt;code&gt;addOneDayToDate&lt;/code&gt; compared to spread &lt;code&gt;dateFns.addDays(currentDate, 1)&lt;/code&gt; everywhere is better because the function depends on DateFns, which is centralized, easy to test, and change.&lt;/p&gt;

&lt;h4&gt;
  
  
  Big modules
&lt;/h4&gt;

&lt;p&gt;This is another very subjective topic: I prefer to have a lot of small and single-function modules compared to long ones. I know that a lot of people prefer the opposite so it's mostly a matter of respecting what is important for the team.&lt;/p&gt;

&lt;h4&gt;
  
  
  Code readability
&lt;/h4&gt;

&lt;p&gt;I'm a fan of the &lt;a href="https://www.oreilly.com/library/view/the-art-of/9781449318482/" rel="noopener noreferrer"&gt;The Art of Readable Code&lt;/a&gt; book and after spending 2.5 years working on a big and complex codebase with zero (!!!) tests, I can tell how much code readability is important.&lt;/p&gt;

&lt;p&gt;This also really depends on the number of developers working on a codebase, but I think it's worth investing in some shared coding patterns that must be enforced in PRs (or even better if they can be automated through Prettier or similar tools).&lt;/p&gt;

&lt;p&gt;I publicly shared the ones we were using in WorkWave in this 7-article series: &lt;a href="https://dev.to/noriste/routemanager-ui-coding-patterns-generic-ones-4iaa"&gt;RouteManager UI coding patterns&lt;/a&gt;. The internal rule we had was that "patterns must be recognizable in the code, but not authors".&lt;/p&gt;

&lt;p&gt;No silver bullets here, the important thing IMO is that readability and refactorability are kept in mind by everyone when writing code.&lt;/p&gt;

&lt;h4&gt;
  
  
  Uniformity is better than perfection
&lt;/h4&gt;

&lt;p&gt;If you are about to refactor a module but you do not have time to refactor also the two modules that are coupled to it... Consider &lt;strong&gt;not refactoring&lt;/strong&gt; it to leave the three modules uniform among them (uniformity means predictability and less ambiguity).&lt;/p&gt;

&lt;h3&gt;
  
  
  Working flow
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Not tracking architectural decisions
&lt;/h4&gt;

&lt;p&gt;Architectural decisions and changes are key to comprehending why a project was designed and how it evolved over time. Usually, these decisions are not 100% reflected in the codebase since big codebases always require incremental approaches.&lt;/p&gt;

&lt;p&gt;It is important to track those decisions to avoid dealing with approaches partially applied, refactors partially done, etc., without a precise idea about the timeline of those decisions and what they were trying to solve.&lt;/p&gt;

&lt;p&gt;Usually, this problem explodes when the engineers who remember those decisions leave the company, and the new ones are doomed to remain ignorant forever.&lt;/p&gt;

&lt;p&gt;On a small scale, this also refers to the awkward changes and/or shenanigans you make to get something done. See this great &lt;a href="https://twitter.com/JoshWComeau/status/1695066496450666995?t=KTOR5A-TWnAh-SHGyHlPUA&amp;amp;s=19" rel="noopener noreferrer"&gt;Josh W. Comeau's example&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1695066496450666995-468" src="https://platform.twitter.com/embed/Tweet.html?id=1695066496450666995"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1695066496450666995-468');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1695066496450666995&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;h4&gt;
  
  
  No PR description and big PRs
&lt;/h4&gt;

&lt;p&gt;That's such an important topic that I wrote four articles about it. Start with the most important one: &lt;a href="https://dev.to/noriste/support-the-reviewers-with-detailed-pull-request-descriptions-2khn"&gt;Support the Reviewers with detailed Pull Request descriptions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you are curious you can dig into some real-life examples I documented here&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/a-case-history-analysing-hasura-consoles-code-review-process-45kk"&gt;A Case History: Analysing Hasura Console's code review process&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/re-building-a-branch-and-telling-a-story-to-ease-the-code-review-485o"&gt;https://dev.to/noriste/re-building-a-branch-and-telling-a-story-to-ease-the-code-review-485o&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/improving-hasuras-internal-pr-review-process-1ham"&gt;Improving Hasura's Internal PR Review process&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Suggesting big changes and approaches during code reviews
&lt;/h4&gt;

&lt;p&gt;PRs are not the best place to suggest big changes or completely change the approach because &lt;strong&gt;you are indirectly blocking releasing a feature or a fix&lt;/strong&gt;. Sometimes is crucial to do it, but maybe the initial analysis and estimation steps, pair programming sessions etc. works better to help shape the approach and the code.&lt;/p&gt;

&lt;h4&gt;
  
  
  When to fix the technical debt?
&lt;/h4&gt;

&lt;p&gt;That's a great question, no silver bullet here... I could only share my experience until now&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In WorkWave we were used to &lt;strong&gt;dealing with technical debt on a daily basis&lt;/strong&gt;. Fixing tech debt is part of the everyday engineers' job. This can slow down the feature development in favour of having a deep knowledge of the context and keeping the codebase in a good shape. It's like knowing that you are slowing down today's development to keep tomorrow's development at the current pace.&lt;/li&gt;
&lt;li&gt;In Hasura, we cannot deal with technical debt due to the needs to deliver new features. This transformed in a lot of frontenders going slower compared to their potential, sometimes introducing bugs, and offering an imperfect UX to the customers. It happened after years, obviously.&lt;/li&gt;
&lt;li&gt;In Preply, engineers can dedicate 20% of their time to tech excellence initiatives, some of them driven by the company and other ones proposed by the teams themselves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can read more about a good example of Hasura's problems in my &lt;a href="https://dev.to/noriste/frontend-platform-use-case-enabling-features-and-hiding-the-distribution-problems-12mi"&gt;Frontend Platform use case - Enabling features and hiding the distribution problems&lt;/a&gt; article. Also, you could read &lt;a href="https://dev.to/noriste/hasura-e2e-tests-chronicles-february-2023-24ki"&gt;what happened to our E2E tests here&lt;/a&gt; after all the tech debt problems we were facing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Major product changes and refactors
&lt;/h4&gt;

&lt;p&gt;Major product changes (the complete rewrite of WorkWave RouteManager, Preply's complete rebrand, etc.) are also perfect for introducing refactors or clearing tech debt that has been there for ages. The reason is that all the knowledge accumulated in the previous years gives us a more comprehensive vision of what is needed and what needs to be cleared, leaving the new product way better than when it started (a sort of greenfield project inside an existing product).&lt;/p&gt;

&lt;h4&gt;
  
  
  No front-end oriented back-end APIs
&lt;/h4&gt;

&lt;p&gt;By "no front-end oriented" I mean APIs not designed with the end customers' UX in mind and a lot of complexity pushed to the front-end in order to keep the back-end development lean (ex. Embedding a lot of DB queries in the front-end avoiding exposing a new API from the back-end). This approach is natural during the initial evolution of a product but will lead to more and more complex front-ends when the product needs to scale.&lt;/p&gt;

&lt;h4&gt;
  
  
  Never updating the NPM dependencies
&lt;/h4&gt;

&lt;p&gt;Again, based on my own experiences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In WorkWave I used to updating the external dependencies on a weekly basis. Usually, it takes me 30 minutes, sometimes 4 hours.&lt;/li&gt;
&lt;li&gt;In Hasura, we were used not to update them, finding ourselves, enabling &lt;code&gt;legacy-peer-deps&lt;/code&gt; by default, leveraging NPM's &lt;code&gt;overrides&lt;/code&gt; and being unable to update any GraphQL-related dependency. Other than having a lot of PRs that completely break the build because of a new dependency.&lt;/li&gt;
&lt;li&gt;In Preply, the outdated TypeScript version made impossible to enable &lt;code&gt;exactOptionalPropertyTypes&lt;/code&gt; and &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; which caused more than one production incident. At the same time, the need to become SOC2 compliant (necessary to expand in the B2B market) got caring about the dependencies a first-class citizen (after a big months-long initiative to update all of them).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And since maintaining dependencies has a cost, you should carefully consider if you really need an eternal dependency or not. Is it maintained? Does it solve a complex problem I prefer to delegate to an external part?&lt;/p&gt;

&lt;p&gt;As an alternative approach, valid only for very very small projects, you can also consider to copy/paste the code of some dependencies inside a "vendor" directory, linking the original project and tracking which version the code belongs to (at the cost of not being able to update it and that other must know they should not install the same dependency).&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Bad practice: Generic TypeScript types and optional properties
&lt;/h4&gt;

&lt;p&gt;It is very common to find types like this&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;
  &lt;span class="nx"&gt;expectedDelivery&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
  &lt;span class="nx"&gt;deliveredOn&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that should be represented with a discriminated union like this&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inProgress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="na"&gt;expectedDelivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="na"&gt;expectedDelivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
  &lt;span class="na"&gt;deliveredOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that is more verbose but acts as pure domain documentation, removes tons of ambiguity, and allows writing better and clearer code.&lt;/p&gt;

&lt;p&gt;The topic is so important and has so many great advantages that I wrote a dedicated article to the topic: &lt;a href="https://dev.to/noriste/how-i-ease-the-next-developer-reading-my-code-1986"&gt;How I ease the next developer reading my code&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Type assertions (&lt;code&gt;as&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Type assertions are a way to tell TypeScript "shut up, I know what I'm doing" but the reality is that barely you know what you are doing, especially thinking about the consequences of what you are doing...&lt;/p&gt;

&lt;p&gt;This happens very frequently in tests, where big objects are "typed" with type assertions... Resulting in the object going outdated compared to the original type... But you realize it only when the tests will fail and you now left room for a lot of future doubts about the test failures...&lt;/p&gt;

&lt;p&gt;The solution: type everything correctly and eventually prefer &lt;code&gt;@ts-expect-error&lt;/code&gt; with an explanation of the error you expect.&lt;/p&gt;

&lt;p&gt;Read &lt;a href="https://www.bytelimes.com/why-you-should-avoid-type-assertions-in-typescript/" rel="noopener noreferrer"&gt;Why You Should Avoid Type Assertions in TypeScript&lt;/a&gt; to know more about the topic (and keep in mind that also the &lt;code&gt;JSON.parse&lt;/code&gt; example shown there can be typed by using &lt;a href="https://zod.dev/?id=parse" rel="noopener noreferrer"&gt;Zod parsers&lt;/a&gt;).&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;@ts-ignore&lt;/code&gt; instead of &lt;code&gt;@ts-expect-error&lt;/code&gt; and broad scope
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;@ts-expect-error&lt;/code&gt; issues could be auto-fixable in the future, compared to &lt;code&gt;@ts-ignore&lt;/code&gt; (that's another way to shut up TypeScript).&lt;/p&gt;

&lt;p&gt;More, &lt;code&gt;@ts-expect-error&lt;/code&gt; should be applied to the smallest possible scope to TS accepting unintended errors.&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;// ❌ don't&lt;/span&gt;
&lt;span class="c1"&gt;// @ts-expect-error TS 4.5.2 does not infer correctly the type of typedChildren.&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cloneElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;typedChildren&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;htmlAttributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- the whole line is impacted by @ts-expect-error&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ do&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cloneElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;// @ts-expect-error TS 4.5.2 does not infer correctly the type of typedChildren.&lt;/span&gt;
  &lt;span class="nx"&gt;typedChildren&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- only typedChildren is impacted by @ts-expect-error&lt;/span&gt;
  &lt;span class="nx"&gt;htmlAttributes&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;any&lt;/code&gt; instead of &lt;code&gt;unknown&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;TypeScript's &lt;code&gt;any&lt;/code&gt; gives you freedom (that's generally bad) of doing everything you want with a variable, while &lt;code&gt;unknown&lt;/code&gt; forces you to strictly guarantee runtime the runtime value before consuming it. &lt;code&gt;any&lt;/code&gt; is like turning off TypeScript while &lt;code&gt;unknown&lt;/code&gt; is like turning on all the possible TypeScript alerts.&lt;/p&gt;

&lt;h4&gt;
  
  
  ESLint rules kept as warnings
&lt;/h4&gt;

&lt;p&gt;ESLint warnings are useless, they only add a lot of background noise and they are completely ignored. Rules should be on or off, but never warnings.&lt;/p&gt;

&lt;h4&gt;
  
  
  Validating the external data
&lt;/h4&gt;

&lt;p&gt;In the software world, the rule of "never trust what the frontend sends to the backend" is crucial, but I'd say that in a front-end application armed with TypeScript types, you should not trust any kind of external data. Server responses, query strings, local storage, JSON.parse, etc. are potential sources of runtime problems if not validated through type guards (read my &lt;a href="https://dev.to/noriste/keeping-typescript-type-guards-safe-and-up-to-date-a-simpler-solution-ja3"&gt;Keeping TypeScript Type Guards safe and up to date&lt;/a&gt; article) or, even better, Zod parsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;

&lt;h4&gt;
  
  
  HTML templating instead of clear JSX
&lt;/h4&gt;

&lt;p&gt;JSX which includes a lot of conditions, loops, ternaries, etc. are hard to read and sometimes unpredictable. I call it "HTML templating". Instead, smaller components with a clear separation of concerns among the components are a better way to write clear and predictable JSX.&lt;/p&gt;

&lt;p&gt;Again, I touched on this topic in my &lt;a href="https://dev.to/noriste/how-i-ease-the-next-developer-reading-my-code-1986"&gt;How I ease the next developer reading my code&lt;/a&gt; article.&lt;/p&gt;

&lt;h4&gt;
  
  
  Lot of React hooks and logic in the component's code
&lt;/h4&gt;

&lt;p&gt;I'm a great fan of hiding the React component's logic into custom hooks whose name clearly indicates the scope of the hook and the consuming it inside it. The reason is always the same: long code before the JSX makes reading the JSX harder.&lt;/p&gt;

&lt;h4&gt;
  
  
  Components accepting &lt;code&gt;className&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Components are designed to encapsulate and hide some logic and give the external world the result of this logic. Their UI is part of the encapsulated APIs the consumer should not be able to change. Usually, components also accept &lt;code&gt;className&lt;/code&gt; to allow consumers to customize small parts of the component's UI (this is the initial goal). Instead, the result is an uncontrolled and hard-to-predict backdoor to rape all the UI details of the components and their children in seconds.&lt;/p&gt;

&lt;p&gt;Like all the JavScript details, the styling details should be encapsulated and hidden, exposing only some generic configurations to the consumer. These configurations explicitly mark what the component offers and what the consumer wants to obtain (like &lt;code&gt;variants&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;p&gt;As a refactorer, when you see a component accepts &lt;code&gt;className&lt;/code&gt;, you already know how hard your life will be.&lt;/p&gt;

&lt;h4&gt;
  
  
  Hide stores implementation details
&lt;/h4&gt;

&lt;p&gt;Something I saw working like a charm on WorkWave RouteManager is hiding the stores under modules that export React-only, store-independent APIs. We started using Recoil way before the rest of the world, and later on, we migrated to Valtio because it better covered our needs. The migration was painless because Recoil was just an implementation detail of modules that export pure React APIs like &lt;code&gt;useSelection.&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Consuming Swiss army knifes
&lt;/h4&gt;

&lt;p&gt;Some React components are generic by design due to the huge number of cases they manage (think of a table, a date picker, a modal, etc.). This makes hard to track what consumers need out of the 100 features those generic components do. As a result, refactoring those components or the consumers is hard. My suggestion is to create intermediate and vertical components that act as proxies for the more complex ones.&lt;/p&gt;

&lt;p&gt;The vertical components' name and description allow the reader to understand what they do and need without digging into the details of how the original complex components are consumed (for instance: &lt;code&gt;UserList&lt;/code&gt; which uses just the sorting options of &lt;code&gt;Table&lt;/code&gt; is clearer compared to digging into how &lt;code&gt;Tutors&lt;/code&gt;, &lt;code&gt;Students&lt;/code&gt;, and &lt;code&gt;Managers&lt;/code&gt; pages use &lt;code&gt;Table&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Bad tests
&lt;/h4&gt;

&lt;p&gt;As a test lover and instructor (I teach about front-end testing at private companies and conferences) I can say that bad tests are the result of lacking experience on this topic, and the only solution is help, mentoring, help, mentoring, help, mentoring, etc.&lt;/p&gt;

&lt;p&gt;Anyway, the false confidence that tests can offer is a big problem in every codebase.&lt;/p&gt;

&lt;p&gt;I suggest reading two of my articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/from-unreadable-react-component-tests-to-simple-stupid-ones-3ge6"&gt;From unreadable React Component Tests to simple, stupid ones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/improving-ui-testss-code-to-ease-debugging-them-later-2478"&gt;Improving UI tests' code with debugging in mind&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  E2E tests everywhere
&lt;/h4&gt;

&lt;p&gt;E2E tests do not scale well because of the need for real data, a real back-end, etc.&lt;/p&gt;

&lt;p&gt;From this perspective, Preply is a great example (and the only successful one I saw in my working life) of what you can achieve when the user experience is considered crucial by the leadership: E2E test are mandatory and the strong Continuous Delivery approach lead to 98%+ stability of the E2E test suite that ensure a lot of happy path are always functional.&lt;/p&gt;

&lt;p&gt;Also, in this case, I suggest reading some of my articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/decouple-the-back-end-and-front-end-test-through-contract-testing-112k"&gt;Decouple the back-end and front-end test through Contract Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/improving-ui-testss-code-to-ease-debugging-them-later-2478"&gt;Improving UI tests' code with debugging in mind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/front-end-productivity-boost-cypress-as-your-main-development-browser-5cdk"&gt;Front-end productivity boost: Cypress as your main development browser&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Developer Experience
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Deprecated APIs
&lt;/h4&gt;

&lt;p&gt;When code is marked as &lt;code&gt;@deprecated&lt;/code&gt;, the IDE shows it as strikethrough'ed and present the documentation, helping the developers realize that they should not use it.&lt;/p&gt;

&lt;p&gt;An example:&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="cm"&gt;/**
 * @deprecated Please use the new toast API /new-components/Toasts/hasuraToast.tsx
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showNotification&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h4&gt;
  
  
  Care about the browser logs
&lt;/h4&gt;

&lt;p&gt;Console warnings (coming from ESLint, from TypeScript, from React, from Storybook, etc.) add a lot of background noise that mixes with the important logs you could trace. Care and remove them in order to avoid the developers ignoring your own important alerts due to the high noise.&lt;/p&gt;

&lt;h4&gt;
  
  
  Developer alerts for unexpected things
&lt;/h4&gt;

&lt;p&gt;Runtime things (ex. server responses) could not be aligned with the front-end types. If you do not want to break the user flow by throwing an error, at least track the error through something that could alert you about it (like Sentry, or whatever other tool), so a short time will pass between the error coming out and you fixing it.&lt;/p&gt;

&lt;h4&gt;
  
  
  React-only APIs
&lt;/h4&gt;

&lt;p&gt;If you are creating an internal library, prefer to expose only React APIs. The big advantage is that you count on React's reactivity system, and managing dynamic/reactive cases in the future will be easier because you are sure the consumers of your React APIs are re-rendered for free and always deal with fresh data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Non-straightforward CI scripts
&lt;/h4&gt;

&lt;p&gt;CI pipelines should just launch scripts present in the package.json without additional logic that increases the cognitive load and makes it harder to replicate errors locally or in another environment. Think about the painful process of trying to decipher what a CI step does to replicate it locally to dig into the root cause of the issue. Maybe CI uses a tool you do not master, maybe CI uses a particular configuration, and all these take you depending on other colleagues/teams who own everything CI.&lt;/p&gt;

&lt;p&gt;CI pipeline should only care about setting up everything with the correct Node.js version (set by the frontenders who maintain the codebase) and launching some CI-dedicated scripts (for instance: &lt;code&gt;ci:lint&lt;/code&gt;, &lt;code&gt;ci:build&lt;/code&gt;, &lt;code&gt;ci:ts-check&lt;/code&gt;, &lt;code&gt;ci:test:unit&lt;/code&gt;, &lt;code&gt;ci:test:e2e&lt;/code&gt;, etc.). This decouple the scripts launched in CI from who knows better the JS ecosystem, getting everything simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit where credit is due
&lt;/h2&gt;

&lt;p&gt;Thank you so much to &lt;a href="https://www.linkedin.com/in/matteoronchi/" rel="noopener noreferrer"&gt;M. Ronchi&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/nicolas-b-47180314b/" rel="noopener noreferrer"&gt;N. Beaussart&lt;/a&gt; for teaching me so many important things in the last few years ❤️ a lot of content included in this article comes from working with them on a daily basis ❤️&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Frontend Platform use case - Acting before a real Product need</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Wed, 31 May 2023 13:05:05 +0000</pubDate>
      <link>https://forem.com/noriste/frontend-platform-use-case-acting-before-a-real-product-need-c38</link>
      <guid>https://forem.com/noriste/frontend-platform-use-case-acting-before-a-real-product-need-c38</guid>
      <description>&lt;p&gt;This article is about what happens when you anticipate Platform activities before a real company need arises.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@bigmck56?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Trevor McKinnon&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/KU4xurtYjT4?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;In the Hasura Console, Datadog was initially used to track some internal errors and alerts. Then, the plan was to move to Sentry and, in parallel, to &lt;a href="https://www.heap.io/" rel="noopener noreferrer"&gt;Heap&lt;/a&gt; to allow the company to create funnels and track user activities to base future Product decisions on data. The company could opt for other tools in the future, obviously.&lt;/p&gt;

&lt;p&gt;This changes in terms of tools and not having a unified direction about how to practically implement them in the Console UI (the main Hasura's front-end project) led to a lot of patterns used by different developers.&lt;/p&gt;

&lt;p&gt;More: since &lt;strong&gt;Hasura treats very sensitive data&lt;/strong&gt; (the env secrets and database details of the clients) it's crucial that this data are not leaked to third-party services.&lt;/p&gt;

&lt;p&gt;The sensitive data problem and the data redaction applies specifically to how Hasura uses Heap, since by default Heap (and a lot of other similar tools) tracks everything the users do in the browser to allow later creating and analysing funnels. At the same time, the Console UI exposes some sensitive data (the database table names, for example) in some HTML attributes (some &lt;code&gt;id&lt;/code&gt;s, some &lt;code&gt;data-testid&lt;/code&gt;s, etc.) that must not be shared with third-party services.&lt;/p&gt;

&lt;p&gt;When Heap has been added to the Console UI, &lt;strong&gt;Heap auto-tracking has been disabled&lt;/strong&gt; in favor of manually adding some dedicated HTML attributes (ex. &lt;code&gt;data-heap&lt;/code&gt;) that then allow the stakeholders to create funnels like "the user clicked on the element that responds to the [data-heap='Save action'] selector". This is a great thing for engineers too, because knowing what the users do in the Console UI allows also to start discussions with stakeholders about "eventually &lt;strong&gt;removing some not-so-used features that force us to keep some not-so-maintained dependencies&lt;/strong&gt; and some not-so-modern code".&lt;/p&gt;

&lt;p&gt;This approach is safe from a data leak perspective, but does not scale from a feedback loop point of view. Due to the internal release management, normally no less than two weeks pass from a stakeholder saying "I'd like to track this button" and the new HTML attribute present in the Hasura products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;In the Platform team (I was one of the frontenders of the Platform team, along with &lt;a href="https://github.com/beaussan" rel="noopener noreferrer"&gt;N. Beaussart&lt;/a&gt; and &lt;a href="https://github.com/nicoinch" rel="noopener noreferrer"&gt;N. Inchauspé&lt;/a&gt;) we decided to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create some analytics-related React APIs to centralize adding the tracking HTML attributes and easily switch from a tracking tool to another one in the future without touching the whole codebase&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Move the data redaction from the app-level to every single feature level, in order to simplify the work of every Feature team. Every Feature team then had to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Analyse the data leaked by their own features in the HTML attributes&lt;/li&gt;
&lt;li&gt; Remove the data leak&lt;/li&gt;
&lt;li&gt; Remove the data-redaction from every single feature&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;As a result, the stakeholder could gain automatic tracking and focus on more interesting things like deciding if to expand tracking to other distributions (like on-prem) or not.&lt;/p&gt;

&lt;p&gt;The first point was the easy part, the developers working on the Console easily adapted to use the new &lt;code&gt;&amp;lt;Analytics/&amp;gt;&lt;/code&gt;, &lt;code&gt;getAnalyticsAttributes&lt;/code&gt;, &lt;code&gt;programmaticallyTraceError&lt;/code&gt; APIs etc.&lt;/p&gt;

&lt;p&gt;The second point, instead, required a lot of refactors, dig into the features code (not something very easy in the Hasura Console), and &lt;strong&gt;a lot of manual tests&lt;/strong&gt; to be sure no features are broken. But this is the kind of tasks for the Platform team, right? Especially if you think about unleashing the Feature teams' potential in terms of re-enabling the auto tracking, right? Well, not exactly...&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it did not work
&lt;/h2&gt;

&lt;p&gt;The work every Feature teams should have done (fixing leaking sensitive data and reenabling the auto tracking) requires time, and this time should be planned and prioritised. What happened is that I found myself pushing for this activity, the Head of Analytics pushing too... But the company had not a strong interest in prioritising everything analytics compared to other activities!&lt;/p&gt;

&lt;p&gt;Not all the work is wasted! In fact, the the new React APIs are in use and they leave room for future evolutions. But &lt;strong&gt;the most impacting process&lt;/strong&gt;, removing the block to auto-tracking, &lt;strong&gt;remained uncompleted despite the initial big effort from the Platform team&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I wanted to share this experience because it is the perfect example for when a Platform team does something not very coupled to the Product goals. This kind of problem could happen especially if the Platform team has not a Product Manager, something I spoke about in my &lt;a href="https://dev.to/noriste/frontend-platform-use-case-creating-a-roadmap-without-a-product-manager-2fdd"&gt;Frontend Platform use case - Creating a roadmap without a Product Manager&lt;/a&gt; article.&lt;/p&gt;

</description>
      <category>frontendplatform</category>
    </item>
    <item>
      <title>Frontend Platform use case - Enabling features and hiding the distribution problems</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Wed, 31 May 2023 06:19:51 +0000</pubDate>
      <link>https://forem.com/noriste/frontend-platform-use-case-enabling-features-and-hiding-the-distribution-problems-12mi</link>
      <guid>https://forem.com/noriste/frontend-platform-use-case-enabling-features-and-hiding-the-distribution-problems-12mi</guid>
      <description>&lt;p&gt;Like other similar products, Hasura is distributed and can be consumed in a lot of different ways, due to different client’s needs and the evolution of the product during the last years.&lt;/p&gt;

&lt;p&gt;The lack of a centralized way to identify the server type and version highly impacts working on the Hasura Console (the most important Hasura’s front-end product) and enabling a feature or not. Things get even worse when the distribution matrix crosses the pricing tiers and their frequent changes.&lt;/p&gt;

&lt;p&gt;Here is a walkthrough the plan to tackle the problem by offering new React APIs to the Hasura frontenders.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/pt-br/@barkiple?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;John Barkiple&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/l090uFWoPaI?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Hasura Console identifies the server "type"
&lt;/h2&gt;

&lt;p&gt;The Hasura Console "assets" (CSS, JS, static files) are in charge of the Console build tools but the HTML serving them unfortunately is not. The various servers were used to add some logic and some environment variables in the global scope (populating a &lt;code&gt;window.__env&lt;/code&gt; object), load the Console assets, and the Console can consume the &lt;code&gt;window.__env&lt;/code&gt; object and decide what to show to the users and what not.&lt;/p&gt;

&lt;p&gt;How the abovementioned situation became such a big problem for the Console developers? Because of the Hasura distribution options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;Hasura server embeds the Console assets&lt;/strong&gt; in order to allow the clients to work offline&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;At the same time, the Hasura server always &lt;strong&gt;tries to load the assets from the CDN&lt;/strong&gt; before fallback to the embedded ones (to fix hotfixes on the fly)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Hasura CLI acts as a server&lt;/strong&gt; that in turn talks to the Hasura server. When served by the Hasura CLI, the Console talks with the Hasura CLI and the Hasura server based on the actions to do&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is no way to force the clients to update their Hasura CLI, resulting in a lot of outdated distributions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In either the Hasura Server or Hasura CLI, the client could be logged as an "Enterprise" user&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;Hasura Console can be served by Lux&lt;/strong&gt; too, the server behind Hasura Cloud&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hasura Cloud has different pricing tiers that evolved over the years&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hasura was introducing a new "Enterprise" distribution and licensing model that differs from the previous one&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you mix it with the fact that&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;window.__env&lt;/code&gt; object is not documented nor typed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The domain names for the plans themselves are not clear (what a "Pro" Console is is a source of confusion too)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to launch the application in all the modes/types (they turned out to be 24 different combinations) is hard&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some "new" server implementations presented some "old" domain names with the goal of easing the Console's developer life&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;you will not be surprised if there were a lot of creative ways to identify the current mode/plan, including a lot of duplication, a lack of centralized management, and a lot of subsequent PRs needed to adjust enabling features after the release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature-first APIs
&lt;/h2&gt;

&lt;p&gt;During one of the internal Front-end Office Hours, we discussed the generic idea to &lt;strong&gt;stop dealing with plans/modes/types, and creating some feature-first APIs&lt;/strong&gt; that hide the implementation details of identifying the plans and that allows the developers to simply tell something like "I need to show this feature in Cloud, that's all". Since this was one of the most reported problems by the frontenders (if you are curious about how we identified the problems to work on, read &lt;a href="https://dev.to/noriste/frontend-platform-use-case-creating-a-roadmap-without-a-product-manager-2fdd"&gt;Frontend Platform use case - Creating a roadmap without a Product Manager&lt;/a&gt;, we (the Platform team) immediately jumped on it.&lt;/p&gt;

&lt;p&gt;The generic plan was something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quickly creating a POC&lt;/strong&gt; to be discussed internally and understand if it would have fulfilled the desired needs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identifying all the existing modes/plans/types/tiers impacting the Console/features and documenting them&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Analyzing the environment variables and APIs served by the different servers could have helped us&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implementing a new React wrapper (&lt;code&gt;&amp;lt;LoadHasuraPlan /&amp;gt;&lt;/code&gt;) around the application that identifies the "Hasura plan"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dealing with just one/two Hasura plans&lt;/strong&gt; to later iterate and all the other plans&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating the "feature-first" APIs: some React components and hooks that allow to easily deal with when a feature is enabled or not (and if not, also telling "why" to the Hasura developers to allow them to propose some alternative or upselling UIs)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Creating some testing and, especially, Storybook utilities&lt;/strong&gt; to help the Hasura developers to simulate all the edge cases straight from their working tool of choice&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easing the Hasura developers to add and configure new features&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easing the Hasura developers to add and configure new ways to detect the user/license properties&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactoring the existing implementations&lt;/strong&gt; of the most important one/two plans to dogfood the new APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Presenting the APIs to Hasura developers to gain their feedback and keeping them updated about the current progress of the new APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Going back to point 5 to deal with all the Hasura plans one by one&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(maybe) Sitting at a table with the back-end developers to expose a new and documented API dedicated to moving the implementation details from the Console to the server&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me elaborate on some of the above steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  The high-level APIs
&lt;/h3&gt;

&lt;p&gt;At a very high level, this diagram summarizes the new APIs and how to use them from the App.&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%2F74npcnul3xo1z72dvivf.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%2F74npcnul3xo1z72dvivf.png" alt="The high-level React APIs and implementation details graph" width="800" height="315"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;(since dev.to compresses the images, here it the link to &lt;a href="https://excalidraw.com/#json=MW6t43v0kUadn3a5wxOVG,qwUFgrqOjJ-7kXkr2_59dA" rel="noopener noreferrer"&gt;the original Excalidraw&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A &lt;code&gt;&amp;lt;LoadHasuraPlan /&amp;gt;&lt;/code&gt; wrapper&lt;/strong&gt; that identifies the Hasura plan and wraps the app with a React Context Provider whose value is a Zustand store that stores the Hasura plan details&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A &lt;code&gt;useIsFeatureEnabled&lt;/code&gt; React hook&lt;/strong&gt; usable everywhere in the app that received the name of the feature, retrieves the Zustand store from the React Context and passes both the store and the feature object to a &lt;code&gt;checkCompatibility&lt;/code&gt; function&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All the implementation details are just a sort of state machine to identify the plan and a lot of if statements to check if the feature is enabled for the current Hasura plan (the TypeScript part includes more complexity, you can find more info about it later in the article)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Please note: despite Zustand allowing us to expose Vanilla JS APIs, &lt;strong&gt;we decided from the very beginning to expose only React APIs&lt;/strong&gt;. The big advantage is that if we offer only React APIs, we can count on React's reactivity system! That means that managing dynamic/reactive cases like "the user activated the license during the Console lifetime" is a no-brainer because we update the store and all the component/hook consumers automatically re-render.&lt;/p&gt;

&lt;p&gt;Otherwise, exposing Vanilla JS APIs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Would have forced us to expose some subscription-like APIs to be sure the API consumers always work with the latest data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Would have opened the doors to a lot of freedom and creativity for all the Console's developers. While speaking about working on big codebases, and speaking from the standpoint of who usually have to later deal with a lot of refactors due to the mentioned creativity... &lt;strong&gt;It is way better to limit the options&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The initial POC
&lt;/h3&gt;

&lt;p&gt;You can find the initial POC &lt;a href="https://github.com/NoriSte/feature-first-hasura-console-poc" rel="noopener noreferrer"&gt;here&lt;/a&gt; but the main goal of creating a POC was&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;To validate the fallback/upselling APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To &lt;strong&gt;validate the idea with the Console developers and to collect their feedback&lt;/strong&gt; since they are the real users of the new APIs and they have all the feature-related complexity in mind&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At &lt;a href="https://github.com/NoriSte/feature-first-hasura-console-poc#feedback" rel="noopener noreferrer"&gt;the end of the POC's README&lt;/a&gt;, you can find all the feedback we collected (asking for different API shapes, asking about the Feature Flags, etc.).&lt;/p&gt;
&lt;h3&gt;
  
  
  Identifying all the existing modes/plans/types/tiers impacting the Console/features
&lt;/h3&gt;

&lt;p&gt;This step required a lot of back and forth with a lot of other stakeholders to identify the available options, all the differences, some of the design choices behind the current state, etc. Then, I spent some time on a lot of trial and error to document all the cases and elaborate some steps to correctly identify all the Hasura plans. It turned out that the &lt;code&gt;window.__env&lt;/code&gt; object plus a series of three XHR requests in a row (in the worst-case scenario) are enough to correctly identify all 24 possible distribution combinations.&lt;/p&gt;
&lt;h3&gt;
  
  
  Implementing the new &lt;code&gt;&amp;lt;LoadHasuraPlan /&amp;gt;&lt;/code&gt; React wrapper
&lt;/h3&gt;

&lt;p&gt;The main characteristic of the wrapper are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;It can be completely disabled through a &lt;code&gt;passThrough&lt;/code&gt; prop. &lt;strong&gt;This is important to get it merged on the &lt;code&gt;main&lt;/code&gt; branch as soon as possible&lt;/strong&gt; way before the new APIs are ready. Putting everything on &lt;code&gt;main&lt;/code&gt; instead of long-living branches is crucial in terms of maintainability and size of PRs, and it's the logic at the base of Feature Flags for instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It can be enabled through some secret, in-app, developer tools to test it out everywhere at any time. You can refer to the great &lt;a href="https://dev.to/noriste/migrating-a-150k-loc-codebase-to-vite-and-esbuild-how-part-2-3-1c08"&gt;Make your own DevTools&lt;/a&gt; article if to learn how to effectively create something similar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The internal list of static (like the &lt;code&gt;window.__env&lt;/code&gt; object) and dynamic (like the license and the pricing tiers) resources consumed to identify the current Hasura plan must be extended easily by everyone to cover future needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It must be tested thoroughly to be sure it covers all the existing and eventually unknown edge cases. This is crucial because the crazy number of combinations leads for sure to some unmanaged cases and if the wrapper does not handle them could make the Hasura customers block from using the Console!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's why I spent a good amount of time writing a lot of unit tests like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;When the server lacks the server_type, the EE license API fail, and the consoleType is pro, then set the Hasura plan as EE Classic and render the children&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;versionApiResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VersionApiResponsePayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;version&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;rawServerEnvVars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ServerEnvVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;consoleType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Act&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;rest&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;*/v1/version&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;res&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versionApiResponse&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;rest&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;*/v1/entitlement&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;res&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="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoadHasuraPlan&lt;/span&gt; &lt;span class="na"&gt;rawServerEnvVars&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rawServerEnvVars&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RenderHasuraPlanName&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RenderEeLicenseStatus&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;LoadHasuraPlan&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestWrapper&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert&lt;/span&gt;
  &lt;span class="nf"&gt;expect&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;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hasura plan: eeClassic&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="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EE license is not expected in this environment&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="nf"&gt;toBeInTheDocument&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;h3&gt;
  
  
  Creating the "feature-first" APIs
&lt;/h3&gt;

&lt;p&gt;The most interesting part here is not about the APIs themselves but about the hidden TypeScript gymnastics. Let me explain what was the goal we had in mind: think about a feature like OpenTelemetry integration, which compatibility is defined by the following object&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openTelemetry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="na"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="na"&gt;ee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="na"&gt;withLicense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="na"&gt;withoutLicense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&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;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;Compatibility&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;When the developer calls &lt;code&gt;isFeatureEnabled('openTelemetry')&lt;/code&gt; and the current Hasura plan is &lt;code&gt;ce&lt;/code&gt; the result must be, from a TypeScript perspective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  status: 'disabled,
  doMatch: {}, // ← empty object
  doNotMatch: { ee: { withLicense: true } }, // ← does not include `withoutLicense`
  current: { hasuraPlan: { name: 'ce' } },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why have I said, "from a TypeScript perspective"? Because TypeScript means autocompletion for known properties and error for unknown ones.&lt;/p&gt;

&lt;p&gt;Why is it so much important to ensure, at the type level, that the &lt;code&gt;doNotMatch&lt;/code&gt; object does not include the &lt;code&gt;ee.withoutLicense&lt;/code&gt; property? Because the developer must be prevented from doing something like this!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;OpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;doNotMatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useIsFeatureEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openTelemetry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="err"&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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&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="err"&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;doNotMatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;withLicense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="err"&gt;          &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Activate&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;license&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="nx"&gt;OpenTelemetry&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        );
      }

    if (doNotMatch.ee.withoutLicense) &lt;span class="si"&gt;{&lt;/span&gt;

&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// WHAAAT? This edge case does not exist!!!!!! 😱😱 But the developers could try to cover&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// all the edge cases without realizing some of them are impossible!&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// TypeScript could prevent this error by throwing an error that `ee.withoutLicense` does&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// not exist for the `OpenTelemetry` feature! 🎉​​&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="err"&gt;          &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Deactivate&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;license&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="nx"&gt;OpenTelemetry&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        );
      }
    }
  }

  return &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Enjoy OpenTelemetry!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be honest: I am not sure that, in the end, the TypeScript gymnastics we implemented (customizing a couple of utility types of the great &lt;a href="https://github.com/sindresorhus/type-fest" rel="noopener noreferrer"&gt;type-fest&lt;/a&gt;) are worth it, but in the first implementation we kept them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating some testing and Storybook utilities
&lt;/h3&gt;

&lt;p&gt;The Hasura Console developers spend most of their time working in Storybook, hence offering some ad-hoc utilities to write all the component stories for all the different Hasura is crucial in terms of Developer Experience.&lt;/p&gt;

&lt;p&gt;We initially opted for exposing some &lt;strong&gt;Storybook Decorators that allow simulating the Hasura plans&lt;/strong&gt;, without also offering some in-Storybook UI addons to switch between plans at runtime. The goal was to offer something very practical without implementing something expensive before validating the overall idea. So we just:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Expose a &lt;code&gt;hasuraPlanDecorator&lt;/code&gt; from the new &lt;code&gt;IsFeatureEnabled&lt;/code&gt; directory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;hasuraPlanDecorator&lt;/code&gt; accepts the name of the Hasura plan to simulate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Based on the name of the plan, &lt;code&gt;hasuraPlanDecorator&lt;/code&gt; returns a React Context Provider whose value is an externally-initialized Zustand store&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The basic plan is used for every story&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every story can import and customise its own decorators passing a different plan name&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole implementation is something like&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MockedPlan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eeWithoutLicense&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eeWithActiveLicense&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eeWithExpiredLicense&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eeWithDeactivatedLicense&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eeWithLicenseInGracePeriod&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;// --------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// MOCK PROVIDERS APIS&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HasuraPlanMockProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="na"&gt;mockedPlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MockedPlan&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&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;mockedPlan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockedPlan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;      &lt;/span&gt;&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MockStoreContextProvider&lt;/span&gt;
&lt;span class="err"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;createNewStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="err"&gt;            &lt;/span&gt;&lt;span class="na"&gt;hasuraPlan&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;ce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="err"&gt;            &lt;/span&gt;&lt;span class="na"&gt;serverEnvVars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="c1"&gt;// The server env vars (aka window.__env) is not mandatory&lt;/span&gt;
&lt;span class="err"&gt;          &lt;/span&gt;&lt;span class="p"&gt;})}&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MockStoreContextProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
      &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// etc.&lt;/span&gt;

&lt;span class="err"&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;// --------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// STORYBOOKs APIS&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasuraPlanDecorator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockedPlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MockedPlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Story&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="err"&gt;      &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HasuraPlanMockProvider&lt;/span&gt; &lt;span class="nx"&gt;mockedPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mockedPlan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;        &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Story&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;      &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/HasuraPlanMockProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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 every story can simulate a different plan with something like this&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OpenTelemetryStory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ComponentStory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;OpenTelemetryEeRequired&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OpenTelemetryEeRequired&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;OpenTelemetryStory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decorators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;hasuraPlanDecorator&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;eeWithoutLicense&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})];&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- simulating a custom Hasura plan&lt;/span&gt;

&lt;span class="nx"&gt;OpenTelemetryStory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;💠 Primary&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;The same APIs can be leveraged for the unit tests too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refactor the existing implementations of the most important one/two plans to dogfood the new APIs
&lt;/h3&gt;

&lt;p&gt;This is the longest part of the first iteration. The fast way to do that is to simply change the core of all the abstractions that have been created to tackle the problem. But this is suboptimal and does not allow properly dogfooding the new APIs. The right thing to do, instead, is to &lt;strong&gt;deeply understand the existing abstractions, go back to the problem that led to creating the abstractions&lt;/strong&gt; and then solve the problem in an easier way thanks to the new API.&lt;/p&gt;

&lt;p&gt;More: it also means updating all the tests and stories involved to test and document the refactored components and functions.&lt;/p&gt;

&lt;p&gt;The result is hundreds of lines of code removed and a lot of functional (manual) tests required to be sure no features are broken. Unfortunately, it also means digging into some existing bugs that now arise thanks to the full understanding of the different distribution combinations.&lt;/p&gt;

&lt;h3&gt;
  
  
  You could wonder: why not just using Feature Flags?
&lt;/h3&gt;

&lt;p&gt;Because a hypothetical feature flag like "Enable OpenTelemetry for the EE users" is not helpful if you are not able to properly identify who are the EE users. The whole problem presented in this article is specific to identifying plans and hence users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Many thanks to &lt;a href="https://github.com/beaussan" rel="noopener noreferrer"&gt;N. Beaussart&lt;/a&gt; and &lt;a href="https://github.com/nicoinch" rel="noopener noreferrer"&gt;N. Inchauspé&lt;/a&gt; (the frontenders of the Platform team along with me) for supporting me designing and discussing the plan presented in this article 😊&lt;/p&gt;

</description>
      <category>frontendplatform</category>
      <category>refactoring</category>
      <category>technicaldebt</category>
    </item>
    <item>
      <title>Frontend Platform use case - Creating a roadmap without a Product Manager</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Wed, 31 May 2023 06:07:33 +0000</pubDate>
      <link>https://forem.com/noriste/frontend-platform-use-case-creating-a-roadmap-without-a-product-manager-2fdd</link>
      <guid>https://forem.com/noriste/frontend-platform-use-case-creating-a-roadmap-without-a-product-manager-2fdd</guid>
      <description>&lt;p&gt;How we created a roadmap for the Frontend Platform team without clear expectations.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@mmckee30?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Mitch Mckee&lt;/a&gt; on &lt;a href="https://unsplash.com/images/things/lighthouse?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;August 2022, Hasura changes the internal team's structure from horizontal teams (the "Console team" with all the frontenders, the "Server team", etc.) to vertical, full-stack "Feature teams" ones that cover a Product need (the "Native DB team", the "GraphQL Services team", etc.). Also, &lt;strong&gt;a generic "Product Platform team"&lt;/strong&gt; has been created to cover all the Product features that do not fall under the umbrella of the various Feature teams. The Platform's team scope also included dealing with high-level tasks and problems that would have allowed the Feature teams to move faster while developing product features.  &lt;/p&gt;

&lt;p&gt;We (&lt;a href="https://github.com/beaussan" rel="noopener noreferrer"&gt;N. Beaussart&lt;/a&gt;, &lt;a href="https://github.com/nicoinch" rel="noopener noreferrer"&gt;N. Inchauspé&lt;/a&gt;, and &lt;a href="https://github.com/noriste" rel="noopener noreferrer"&gt;I&lt;/a&gt;) were the front-end side of the Platform team and we wanted to ease all the other frontenders' lives by removing some of the huge technical debt that prevent them to work effectively and to release features safely and without introducing a lot of bugs and "fix" PRs.&lt;/p&gt;

&lt;p&gt;We then tried to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Identify what front-end tasks fall under the umbrella of the Platform teams in other companies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identify the most important activities to work on&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Identify what front-end tasks fall under the umbrella of the Platform teams in other companies
&lt;/h2&gt;

&lt;p&gt;Please keep in mind that while Platform teams are very common in a lot of companies, frontenders are not usually included in those teams. As a result, the public resources for front-end Platform activities are limited. Anyway, we found some great resources that it's worth mentioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;And if you need more generic info: this is an introduction about Platform teams from Gergely Orosz. The articles are behind a paywall but they are some of the best out there &lt;a href="https://blog.pragmaticengineer.com/platform-teams/" rel="noopener noreferrer"&gt;https://blog.pragmaticengineer.com/platform-teams/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An article about what allows a Platform team to work well or not &lt;a href="https://www.microtica.com/blog/why-your-platform-engineering-team-isnt-awesome" rel="noopener noreferrer"&gt;https://www.microtica.com/blog/why-your-platform-engineering-team-isnt-awesome&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What does it mean to be in a UI Platform team &lt;a href="https://yiou.me/blog/posts/thinking-in-platform" rel="noopener noreferrer"&gt;https://yiou.me/blog/posts/thinking-in-platform&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A great article about Hubspot platform team &lt;a href="https://product.hubspot.com/blog/frontend-platform" rel="noopener noreferrer"&gt;https://product.hubspot.com/blog/frontend-platform&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Croz experience at building a successful Platform team&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://teamtopologies.com/industry-examples/building-a-successful-platform-team-at-croz" rel="noopener noreferrer"&gt;https://teamtopologies.com/industry-examples/building-a-successful-platform-team-at-croz&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Quora answer about the alignment between Platform teams and Feature teams &lt;a href="https://www.quora.com/How-do-product-platform-engineers-keep-in-touch-with-the-needs-of-product-engineers/answer/Alexis-Larry" rel="noopener noreferrer"&gt;https://www.quora.com/How-do-product-platform-engineers-keep-in-touch-with-the-needs-of-product-engineers/answer/Alexis-Larry&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By reading them, we found confirmations about the tasks covered by frontenders in Platform teams, such as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Fix the most problematic &lt;strong&gt;technical debt&lt;/strong&gt; that slows down the frontenders of the Feature teams&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix the problems that will slow down the Feature teams in the short term by &lt;strong&gt;anticipating the new Product features' needs&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintain &lt;strong&gt;Design Systems&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create some common patterns and best practices and automate getting them respected&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improve and stabilise the various application tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evaluate &lt;strong&gt;new solutions and technologies&lt;/strong&gt; to better solve internal technical problems&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manage and optimise the front-end tools and dependencies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mentor&lt;/strong&gt; other frontenders and spread knowledge&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Due to bug technical debt present in Hasura's front-end projects, how could we (the frontenders of the Platform team) ensure our activities were aligned with the short-term Product needs? How to create a sort of roadmap and stick with it regardless of the everyday problems that could arise from such a complex Product and a lot of engineers? Go ahead 😊&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the most important activities to work on
&lt;/h2&gt;

&lt;p&gt;We proceeded by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Interviewing all the stakeholders&lt;/strong&gt; that are directly or indirectly impacted by the Hasura Console (Hasura's main front-end application and the most problematic one), including&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; All the developers working on it on a daily basis&lt;/li&gt;
&lt;li&gt; All the developers working on it time every now and then&lt;/li&gt;
&lt;li&gt; All the Engineering Managers of the teams including developers from the previous points&lt;/li&gt;
&lt;li&gt; The Documentation team&lt;/li&gt;
&lt;li&gt; The Graphic Design team&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Collecting all the answers in a shared spreadsheet&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Summarising the most requested area of interventions (the "Engineering priorities") and categorising them&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Preparing a presentation&lt;/strong&gt; for the higher-ups that include&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; A slide for every single activity (15 in total) with a clear explanation of why it was important, what the activity is, how many (in percentage) stakeholders asked for it, and a T-shirt size&lt;/li&gt;
&lt;li&gt; A table that acted as the focus of the presentation, where we asked the higher-ups to put a "Product priority" to then mixed with the "Engineering priorities" that we identified&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;The final goal was to list specific activities with&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Engineering "priority"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Product "priority"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The T-shirt size&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And creating a roadmap out of this list was a breeze! This also helped us to identify &lt;strong&gt;what to not work on&lt;/strong&gt;, something not very straightforward when you deal with a lot of problems and requests on a daily basis.&lt;/p&gt;

&lt;p&gt;Here is an example of the table we created with a bunch of activities we identified:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;T-shirt size&lt;/th&gt;
&lt;th&gt;Eng. ⚠&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Perf+stability&lt;/td&gt;
&lt;td&gt;Sentry: Mapping feature directories to teams.&lt;/td&gt;
&lt;td&gt;👕👕&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Providing tools to respect the libraries strategy in the Console.&lt;/td&gt;
&lt;td&gt;👕👕👕&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Update GraphQL and GraphiQL&lt;/td&gt;
&lt;td&gt;👕👕👕👕&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DX&lt;/td&gt;
&lt;td&gt;Better docs and tools to run the Console in all modes/types.&lt;/td&gt;
&lt;td&gt;👕👕&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression&lt;/td&gt;
&lt;td&gt;Storybook Tests: Reduce the drag in testing different Console modes/types.&lt;/td&gt;
&lt;td&gt;👕👕&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The higher-ups highly appreciated the initiative and we immediately started working on some of the activities.&lt;/p&gt;

</description>
      <category>frontendplatform</category>
      <category>roadmap</category>
    </item>
    <item>
      <title>Hasura E2E tests chronicles, February 2023</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Wed, 08 Feb 2023 10:49:22 +0000</pubDate>
      <link>https://forem.com/noriste/hasura-e2e-tests-chronicles-february-2023-24ki</link>
      <guid>https://forem.com/noriste/hasura-e2e-tests-chronicles-february-2023-24ki</guid>
      <description>&lt;p&gt;I joined Hasura in May 2022, and one of my first tasks was to fix the E2E tests of the Hasura Console, main Hasura's front-end application.&lt;/p&gt;

&lt;p&gt;The main problems were: they were slow, and they were flaky. Then, by digging into the topic, there was more to say, more to decide, more to fix, and more to do. Let me elaborate a bit more:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The E2E tests were &lt;strong&gt;slow&lt;/strong&gt;: a lot of &lt;code&gt;cy.wait(10000)&lt;/code&gt; (yes, ten seconds) everywhere.&lt;/li&gt;
&lt;li&gt; The E2E tests were &lt;strong&gt;flaky&lt;/strong&gt;: a couple of months before I joined Hasura, the whole company complained about the CI jobs' flakiness, preventing the teams from merging their PRs. The problem has been workaround'ed by enabling Buildkite (our CI tool) to retry the E2E test job.&lt;/li&gt;
&lt;li&gt; The E2E tests were &lt;strong&gt;cryptic&lt;/strong&gt;. Understanding the E2E tests (and fixing/refactoring them) was hard because they were long, terse, and had many abstractions.&lt;/li&gt;
&lt;li&gt; Debugging the E2E tests was challenging because of the many different modes the Hasura Console can launch.&lt;/li&gt;
&lt;li&gt; The server team used the E2E tests also to &lt;strong&gt;test the server&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Cypress crashed because the Console was too resource-demanding.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's go through every single topic, one by one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@claybanks?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Clay Banks&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/pNEmlb1CMZM?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The E2E tests were slow.
&lt;/h2&gt;

&lt;p&gt;In one of my old articles, &lt;a href="https://dev.to/noriste/await-do-not-make-your-e2e-tests-sleep-4g1o"&gt;"Await, do not make your E2E tests sleep ⏳"&lt;/a&gt;, I shared why having a fixed amount of waiting/sleep is terrible in the tests, following the gist of it.&lt;/p&gt;

&lt;p&gt;Fixed waitings are one of the most common causes of slow tests. The tests get slow by always waiting for 10 seconds when an XHR request is happening, even if it usually takes less than 1 second. And for the rare cases where the XHR request takes more than 10 seconds (imagine a cold server start), the tests will fail.&lt;/p&gt;

&lt;p&gt;Instead, the &lt;strong&gt;test should wait for something that happens deterministically &lt;/strong&gt;(the XHR request, an element to appear, etc.). Cypress eases avoiding sleep with its retry-ability (see &lt;a href="https://docs.cypress.io/guides/core-concepts/retry-ability#Commands-vs-assertions" rel="noopener noreferrer"&gt;the docs here&lt;/a&gt;) and with built-in timeouts, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  up to 60 seconds for a page to trigger the load event when visited&lt;/li&gt;
&lt;li&gt;  up to 4 seconds for an element to appear before interacting with it&lt;/li&gt;
&lt;li&gt;  up to 5 seconds for an XHR request to start and up to 30 seconds to end&lt;/li&gt;
&lt;li&gt;  etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another &lt;strong&gt;critical problem&lt;/strong&gt; of fixed waitings: &lt;strong&gt;the developer cannot understand if the test's creator initially wanted to wait for an XHR request, for an element to appear, or for an animation&lt;/strong&gt;, slowing down the debugging and refactoring processes.&lt;/p&gt;

&lt;p&gt;Guess what? Replacing the sleep with proper "wait for the XHR request to happen" was almost impossible because of a series of problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The Hasura Console comprises legacy and more modern code with no central server-state management. Since different code ages coexist on the same page, other code parts fetch the same data differently. This results in many useless requests and the &lt;strong&gt;impossibility of predicting the order of the XHR requests&lt;/strong&gt;. No order, no party, because Cypress will fail to wait for these crazy requests with many false negatives.&lt;/li&gt;
&lt;li&gt; The Hasura Console can use different servers based on how the consumers use it. The main difference is that the Console can run in server mode (talking to the Hasura GraphQL Engine--HGE-- server) or in CLI mode (talking to a locally running CLI server that, in turn, talks with the HGE server). Different servers mean different URLs, different ports, and different APIs, making it &lt;strong&gt;almost impossible to intercept the XHR requests&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So? I see two leading possible solutions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The one to use most of the time: waiting for something that reflects the fact that the XHR request happened (ex., the success notification, something appearing in the UI) with a longer delay (remember, Cypress waits up to 30 seconds by default for an XHR request and there is a reason), and a comment for the readers that the long delay aims to replace the unfeasibility of intercepting XHR requests.&lt;/li&gt;
&lt;li&gt; Intercepting two possible requests instead of one. Following is a snippet coming from our codebase
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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;http://localhost:8080/v1/metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_action&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createAction&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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;http://localhost:9693/apis/migrate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_action&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createAction&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="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBySel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create-action-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@createAction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;checkMetadataPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interception&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;Action payload&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;3. The one I do not suggest: waiting for some requests to happen before proceeding with the test. Why do not I recommend it? Because of the edge cases to care about and the resulting complex code.&lt;/p&gt;

&lt;p&gt;Following is an example (get ready for something shocking).&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Wait for a bunch of requests to be settled before proceeding with the test.
 *
 * Alternatively, https://github.com/bahmutov/cypress-network-idle could be used
 *
 * This is a workaround for "element is 'detached' from the DOM" Cypress' error (see the issue
 * linked below). Since the UI gets re-rendered because of the requests, this utility ensures that
 * all the requests parallelly made by the UI are settled before proceeding with the test. Hence, it
 * ensure the UI won't re-render during the next interaction.
 *
 * What are the requests that must be awaited? By looking at the Cypress Test Runner, they are the
 * following, made parallelly or in a rapid series.
 * 1. export_metadata
 * 2. export_metadata
 * 3. export_metadata
 * 4. test_webhook_transform
 * 5. test_webhook_transform
 * 6. test_webhook_transform
 * 7. test_webhook_transform
 * At the moment of writing, I'm not sure the number of requests are fixed or not. If they are fixed,
 * using the cy.intercept `times` options would result in a more expressive and less convoluted code.
 *
 * To give you an overall idea, this is a timeline of the requests
 *
 *         all requests start                             all requests end
 *         |                 |                            |               |
 * |--🚦🔴--1--2--3--4--5--6--7----------------------------1--2--3--4--5--6-7--🚦🟢--|
 *
 *
 * ATTENTION: Despite the defensive approach and the flakiness-removal purpose, this function could
 * introduced even more flakiness because of its empiric approach. In case of failures, it must be
 * carefully evaluated when/if keeping it or thinking about a better approach.
 * In general, this solution does not scale, it should not be spread among the tests.
 *
 * @see https://github.com/cypress-io/cypress/issues/7306
 * @see https://glebbahmutov.com/blog/detached/
 * @see https://github.com/bahmutov/cypress-network-idle
 */&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cypress-wait-until&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForPostCreationRequests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;waitCompleted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*--- All requests must be settled*&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;pendingRequests&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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;http://localhost:8080/v1/metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitCompleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*--- Request pending*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;pendingRequests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*--- Request settled*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nx"&gt;pendingRequests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*--- Waiting for the first request to start*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if at least one request has been caught. This check must protect from the following case&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;//            check          requests start           test failure, the requests got the UI re-rendered&lt;/span&gt;
  &lt;span class="c1"&gt;//            |              |                        |&lt;/span&gt;
  &lt;span class="c1"&gt;// |--🚦🔴----⚠️---🚦🟢-------1-2-3-4-5-6-7-1----------💥&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// where checking that "there are no pending requests" falls in the false positive case where&lt;/span&gt;
  &lt;span class="c1"&gt;// there are no pending requests because no one started at all.&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// The check runs every millisecond to be 100% sure that no request can escape (ex. because of a&lt;/span&gt;
  &lt;span class="c1"&gt;// super fast server). A false-negative case represented here&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;//         requests start requests end   check              check               test failure, no first request caught&lt;/span&gt;
  &lt;span class="c1"&gt;//         |            | |           |  |                  |                   |&lt;/span&gt;
  &lt;span class="c1"&gt;// |--🚦🔴--1-2-3-4-5-6-7-1-2-3-4-5-6-7--⚠️------------------⚠️------------------💥&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pendingRequests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 5 seconds is the default Cypress wait for a request to start&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;errorMsg&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 first request caught&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="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*--- Waiting for all the requests to start*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Let pass some time to collect all the requests. Otherwise, it could detect that the first&lt;/span&gt;
  &lt;span class="c1"&gt;// request complete and go on with the test, even if another one will be performed in a while.&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// This fixed wait protects from the following timeline&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;//           1st request start     first request end       other requests start   test failure, the requests got the UI re-rendered&lt;/span&gt;
  &lt;span class="c1"&gt;//           |                     |                       |                      |&lt;/span&gt;
  &lt;span class="c1"&gt;// |--🚦🔴---1---------------------1----🚦🟢----------------2-3-4-5-6-7-1----------💥&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// Obviously, it is an empiric waiting, that also slows down the test.&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*--- Waiting for all the requests to be settled*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pendingRequests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 seconds is the default Cypress wait for the request to complete&lt;/span&gt;
    &lt;span class="na"&gt;errorMsg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some requests are not settled yet&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;waitCompleted&lt;/span&gt; &lt;span class="o"&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;I think it's a pity not to intercept the XHR requests because it could act as a life-saver that, when something is not working, allows you to immediately understand if it's the front-end fault (because of a wrong request payload) or a back-end fault (because of an incorrect response payload, status code, etc.). I can live with it, anyway 😊&lt;/p&gt;

&lt;h2&gt;
  
  
  The E2E tests were flaky
&lt;/h2&gt;

&lt;p&gt;Flakiness is always a red alert. You should monitor your tests and always fix the E2E tests. If you can't fix them now, skip them, and explain why. &lt;strong&gt;Flaky tests undermine the working flow&lt;/strong&gt;, undermine the confidence and trust in the E2E tests, make everyone hate them, and add more friction than the one they are supposed to remove. &lt;strong&gt;No tests are way better than flaky tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On the Hasura Console, not only has the flakiness not been fixed but the significant, involuntarily error that has been made is enabling Buildkite retry mechanisms in case of E2E test failures... without checking what was happening under the hood. Why? Because Buildkite retries the failing E2E tests without passing a different id to the Cypress CLI. As a result, Cypress does not run any test in the retry because it knows that a test run with the same id already happened! &lt;strong&gt;No test run means no failing tests, which means CI goes green&lt;/strong&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%2Faw9ns1brrex33ygfrwv2.jpg" 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%2Faw9ns1brrex33ygfrwv2.jpg" alt="Buildkite showing a failure, retrying the tests, then going green because Cypress does nothing" width="800" height="684"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Buildkite showing a failure, retrying the tests, then going green because Cypress does nothing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you spot the terrible problem? CI is going green for failing tests, with the developers thinking that the tests were succeeding. The result is not only false confidence but also &lt;strong&gt;the E2E tests were less and less aligned with the application&lt;/strong&gt; (highlighting another problem, no one was running the tests locally while working...).&lt;/p&gt;

&lt;p&gt;Another thing to consider: running all the E2E tests, then re-starting Cypress to re-run them means that every PR run the complete test suite, paying for AWS running them and for the Cypress Cloud dashboard, paying the time cost of waiting for the completion, with no value at all!&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%2F9rkmoa49o32d3f4iv8cv.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%2F9rkmoa49o32d3f4iv8cv.png" alt="The Buildkite jobs showing the E2E tests require almost 10 minutes" width="800" height="397"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Buildkite jobs showing the E2E tests require almost 10 minutes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The E2E tests were cryptic
&lt;/h2&gt;

&lt;p&gt;Test code must be 100x simpler than application code. It must be a clear goal when writing tests, not a by-product of writing a program (the test) that's 100x simpler by definition.&lt;/p&gt;

&lt;p&gt;Anyway, this topic is long and deserves a dedicated article. You can find it here: &lt;a href="https://dev.to/noriste/improving-ui-testss-code-to-ease-debugging-them-later-2478"&gt;Improving UI tests' code with debugging in mind&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging the E2E tests was challenging
&lt;/h2&gt;

&lt;p&gt;This is the result of multiple factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The E2E tests are hard to read (look at the previous chapter, "The E2E tests were cryptic").&lt;/li&gt;
&lt;li&gt; The Console runs in different modes (see the "The E2E tests were slow." chapter), which is not easy to detect at first glance.&lt;/li&gt;
&lt;li&gt; The tests are coupled, and test B needs to run after test A. This is another exciting and lengthy debate. I split it into a dedicated article, too. See "&lt;a href="https://dev.to/noriste/one-long-e2e-test-or-small-independent-ones-33ao"&gt;One long E2E test or small, independent ones?&lt;/a&gt;".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But please keep in mind that &lt;strong&gt;debugging E2E tests is always hard&lt;/strong&gt;. The only game changer I know (and suggest) is &lt;a href="https://www.replay.io/" rel="noopener noreferrer"&gt;Replay&lt;/a&gt;, which now &lt;a href="https://docs.replay.io/recording-browser-tests-(beta)" rel="noopener noreferrer"&gt;allows recording browser tests&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server team used the E2E tests also to test the server
&lt;/h2&gt;

&lt;p&gt;On the server, the situation was even worse. There were no integration tests that allowed us to be sure the server always respected the contract. As a result, the Console's E2E tests were also used to check that "if the Console works, the server did not break any API".&lt;/p&gt;

&lt;p&gt;Apart from the false confidence our Buildkite misconfiguration gives, this prevents the Console tests from scaling. It's good to have some E2E tests, but since they are super slow and sometimes flaky, we need to keep them at a minimal number. Most of the testing focus should be on the front-end and back-end sides, but independently from each other.&lt;/p&gt;

&lt;p&gt;You can find all the details and the rationales in the dedicated "&lt;a href="https://dev.to/noriste/decouple-the-back-end-and-front-end-test-through-contract-testing-112k"&gt;Decouple the back-end and front-end test through Contract Testing&lt;/a&gt;" article, which is the proposal I made internally to split the Console and server tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and the future
&lt;/h2&gt;

&lt;p&gt;So, in 2022:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We started refactoring some tests (but never completed them).&lt;/li&gt;
&lt;li&gt; We started the Contract Testing proposal (but have yet to push it seriously).&lt;/li&gt;
&lt;li&gt; We tracked and skipped all the flaky tests, willing to fix the CI misconfiguration (we never fixed it because of other CI problems).&lt;/li&gt;
&lt;li&gt; We enabled Slack alerts from Cypress to quickly identify other flaky tests, skip them or fix them.&lt;/li&gt;
&lt;li&gt; We started using proper Contract Testing methodology on a small portion of features.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt; Then, an internal Working Group was created to fix our numerous CI problems, and the E2E tests were one of the most problematic ones. And since they are slow, they are flaky, they add no value, they are outdated...&lt;/p&gt;

&lt;p&gt;... We opted for the hardest but best decision: to eradicate the E2E tests!&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%2Fdonrsw4xrqbyxcxpsggt.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%2Fdonrsw4xrqbyxcxpsggt.png" alt="The description of the big PR removing the E2E tests" width="800" height="955"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The description of the big PR removing the E2E tests.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's start with a clean slate and think about the future! Some points that will positively impact the E2E tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We will consider &lt;strong&gt;Playwright instead of Cypress&lt;/strong&gt; due to the speed and fewer resources required. The first round of using it went well. Now we need to check if it fulfills all our needs.&lt;/li&gt;
&lt;li&gt; Refactors are happening across some features, pushing &lt;strong&gt;more tests to the interaction level (Storybook)&lt;/strong&gt; level instead of the full-app-in-browser level.&lt;/li&gt;
&lt;li&gt; The recent migration to &lt;strong&gt;Nx reduced the size of the Console by 70%&lt;/strong&gt;, which means a faster startup and faster browser tests.&lt;/li&gt;
&lt;li&gt; We have too many types/modes the Console can run, some of them will be removed from a Product perspective.&lt;/li&gt;
&lt;li&gt; We (the frontenders of the Platform Team) will dedicate time &lt;strong&gt;helping and mentoring&lt;/strong&gt; the frontenders of the Feature teams to write more scalable tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But there is one real game changer: &lt;strong&gt;the server will auto-generate TypeScript types&lt;/strong&gt; for all the server objects and APIs! That means that on the Console, we will always be using the latest and correct types, guaranteed by the server, and &lt;strong&gt;we can start trusting whatever comes from the server&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;Ensuring the server respects its part of the contract means we will not need so many E2E tests, but server-free tests against the server types will be our safety net! This changes everything in terms of the need for a lot of E2E tests!!!&lt;/p&gt;

&lt;p&gt;Stay tuned. Maybe next year, we will share how we are doing in terms of front-end testing ❤️&lt;/p&gt;




&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/improving-ui-testss-code-to-ease-debugging-them-later-2478"&gt;Improving UI tests' code with debugging in mind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/one-long-e2e-test-or-small-independent-ones-33ao"&gt;One long E2E test or small, independent, ones?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/decouple-the-back-end-and-front-end-test-through-contract-testing-112k"&gt;Decouple the back-end and front-end test through Contract Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/hasura-console-ui-coding-patterns-testing-281d"&gt;Hasura Console UI coding patterns: Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/from-unreadable-react-component-tests-to-simple-stupid-ones-3ge6"&gt;From unreadable React Component Tests to simple, stupid ones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/await-do-not-make-your-e2e-tests-sleep-4g1o"&gt;Await, do not make your E2E tests sleep&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>database</category>
      <category>extensions</category>
      <category>performance</category>
    </item>
    <item>
      <title>Improving UI tests' code with debugging in mind</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Tue, 07 Feb 2023 08:47:47 +0000</pubDate>
      <link>https://forem.com/noriste/improving-ui-testss-code-to-ease-debugging-them-later-2478</link>
      <guid>https://forem.com/noriste/improving-ui-testss-code-to-ease-debugging-them-later-2478</guid>
      <description>&lt;p&gt;UI tests are made up of a lot of steps and should accomplish three main things, but two of them are somewhat underestimated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; To test a feature &lt;em&gt;(the obvious one)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; To help the reader to understand what the code does &lt;em&gt;(usually underestimate)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; To ease debuggability &lt;em&gt;(underestimate, and also requires experience)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I go through some simple yet effective tips to keep in mind when writing UI tests.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@brett_jordan?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Brett Jordan&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/wF7GqWA3Tag?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Readability
&lt;/h2&gt;

&lt;p&gt;Code's readability is something I care about a lot (at the end of the article, you can find other pieces of mine related to the same topic).&lt;/p&gt;

&lt;p&gt;One arguable topic (Page-Object Model's fans will get hurt) is my view about abstraction in tests.&lt;/p&gt;

&lt;p&gt;Let's look at a real-life example of a test I had to fix.&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;// spec.ts file&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create Query Action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createQueryAction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// test.ts file (simplified version)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createMutationAction&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nf"&gt;clearActionDef&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;typeIntoActionDef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createMutationActionText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;clearActionTypes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// test.ts file contains the clearActionDef, typeIntoActionDef, etc.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clearActionDef&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{selectall}&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;force&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="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&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;keyCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;which&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;force&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="c1"&gt;// test.ts file contains also the statements&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;createMutationActionText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`type Mutation {
    login (username: String!, password: String!): LoginResponse
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createMutationCustomType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`type LoginResponse {
    accessToken: String!
  }
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createMutationHandler&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://hasura-actions-demo.glitch.me/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rationale behind the short functions is creating small, reusable pieces of code to help other tests that must do similar things on the page.&lt;/p&gt;

&lt;p&gt;I think it is not good: because &lt;strong&gt;it is hard to build a mental model of what the test does&lt;/strong&gt;! All the test parts are split into small functions and utilities, while the test's code must be as straightforward as possible.&lt;/p&gt;

&lt;p&gt;Can you recall the two underrated points at the top of the article? The idea is that the test should accomplish three main things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  To help the reader to understand what code does&lt;/li&gt;
&lt;li&gt;  To allow debugging itself with ease&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The former requires the test's code to be as dumb as possible, and there is no advantage in having abstraction in the test's code because it leads to spending more time debugging and maintaining the tests instead of the application.&lt;/p&gt;

&lt;p&gt;The latter relates to the wrong part of tests: debugging/fixing them. Debugging a UI test is hard because you work with&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You front-end application&lt;/li&gt;
&lt;li&gt;  The browser&lt;/li&gt;
&lt;li&gt;  A tool that controls the browser&lt;/li&gt;
&lt;li&gt;  The instructions you provide to the tool that controls the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of the above elements can fail, and even the more experienced developers can struggle with understanding the source of a failing test.&lt;/p&gt;

&lt;p&gt;Hence, E2E testing is complicated. Cypress improves the developers' life (I discussed it in my &lt;a href="https://dev.to/noriste/some-ui-testing-problems-and-the-cypress-way-1167"&gt;Some UI testing problems and the Cypress way&lt;/a&gt; article), but a straightforward code dramatically helps.&lt;/p&gt;

&lt;h3&gt;
  
  
  No abstraction at all
&lt;/h3&gt;

&lt;p&gt;I recommend having no abstraction at all (later, I will talk about the exceptions and which abstraction it's good)! I rewrote the above example to something like&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test the feature&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actionDefinitionTextarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typeConfigurationTextarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;cy&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;@actionDefinitionTextarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clearConsoleTextarea&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`type Mutation {
        login (username: String!, password: String!): LoginResponse
      }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;force&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="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;cy&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;@typeConfigurationTextarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clearConsoleTextarea&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`type LoginResponse {
      accessToken: String!
    }
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;force&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="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rewritten test does the same as the original one, but when you jump on the test's code, the overhead of jumping back and forth to connect the dots mentally is not needed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Do you want to know that is typed in the textarea? No hassle, it's there!&lt;/li&gt;
&lt;li&gt;  Do you want to know what is the textareas used by the text? No hassle, it's there!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When is abstraction good in tests?
&lt;/h3&gt;

&lt;p&gt;In my opinion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  When I want to hide some test oddities that could distract the readers for no value&lt;/li&gt;
&lt;li&gt;  When they are soft, almost parameters-free, only one-level deep&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An example of a test oddity is the following&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="cm"&gt;/**
 * Clear a Console's textarea.
 * Work around cy.clear sometimes not working in the Console's textareas.
 */&lt;/span&gt;
&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clearConsoleTextarea&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;prevSubject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{selectall}&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;force&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="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&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;keyCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;which&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;force&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;I created the central &lt;code&gt;cy.clearConsoleTextarea&lt;/code&gt; because&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; It's a workaround 😊&lt;/li&gt;
&lt;li&gt; For a newcomer, it is odds reading &lt;code&gt;trigger('keydown')&lt;/code&gt; instead of using the more idiomatic &lt;code&gt;cy.clear&lt;/code&gt;, and I do not want to leave a comment explaining it everywhere.&lt;/li&gt;
&lt;li&gt; The command is made up of 5 lines of code that would get the test's code too long for no reason.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An example of a soft abstraction is the following&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;expectSuccessNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&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="nx"&gt;cy&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;.notification-success&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="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&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="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&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;I like it because&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; It does not use other abstracted code: if my tests fails at &lt;code&gt;expectSuccessNotification('Table created!')&lt;/code&gt; I do not have to go crazy down the rabbit-hole to understand what happens behind &lt;code&gt;expectSuccessNotification&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; It accepts only one variable, not a lot of options; neither includes conditions that would get hard in the way of understanding what the code finally does.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;It's vertical for a specific use case&lt;/strong&gt;. It does not try to cover all the notification types, contents, etc., at once. Other vertical functions will do.&lt;/li&gt;
&lt;li&gt; If you refactor the notification system, you have a central point to refactor to adapt the tests to the new notification system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the opposite, this is what I do not want to have (while speaking about notification utilities)&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;message&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="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;title&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="nl"&gt;message&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="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;
&lt;span class="p"&gt;)&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="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cy&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="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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;.notification-success&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;.notification-error&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="nx"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&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;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&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;I'm not too fond of the above example because&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; It tries to cover too many use cases at a time.&lt;/li&gt;
&lt;li&gt; If it fails, you have to deal with conditions that make the whole experience a nightmare.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can find more best practices we follow internally in the &lt;a href="https://dev.to/noriste/hasura-console-ui-coding-patterns-testing-281d"&gt;Hasura Console UI coding patterns: Testing&lt;/a&gt; article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Matching the test's code and test runner's commands
&lt;/h3&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%2F49iyokcbe19clucyhsmj.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%2F49iyokcbe19clucyhsmj.png" alt="The test code side by side with the Cypress panel with some red arrows to match the code and the Cypress logs" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Cypress Test Runner helps understand what is happening in the application and which commands are executed, but when you debug a test, it is hard to have an immediate correlation between the Test Runner and the code. More, the logs do not help in understanding what the test is doing in feature terms (ex., the logs say "types in the textarea" but do not say "Type in the Type Configuration textarea"). So, detecting the root of a failure is hard. Cypress records videos for the failing tests, but it is useless if the reader/debugger does not have an immediate correlation between the logs and what the test is doing in plain English.&lt;/p&gt;

&lt;h4&gt;
  
  
  Look at something like this
&lt;/h4&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%2Fyujbzw8fu8bbch0ywh0k.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%2Fyujbzw8fu8bbch0ywh0k.png" alt="The code and the Cypress panel side by side, with a lot of custom logs that allow directly connecting what happens in Cypress to a precise point in the code" width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I add a log reporting what the test is doing, allowing a direct correlation between the test's code and the Test Runner (&lt;code&gt;cy.log('**--- Type in the Webhook Handler field**');&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Please note that you can pass more arguments to 'cy.log', and they are logged right in the devtools' console when you click on the logged command.&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%2F8kfijm0i53uetlayvt6y.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%2F8kfijm0i53uetlayvt6y.png" alt="The Cypress Test Runner showing the value of a logged object" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Storybook and Playwright already have the concept of &lt;code&gt;step&lt;/code&gt; utilities that allow explaining in English what is happening in the test. Cypress does not have the same option, so the &lt;code&gt;cy.log&lt;/code&gt; I propose is valuable, in my opinion.&lt;/p&gt;

&lt;p&gt;It's worth mentioning that even if Cypress does not have &lt;code&gt;step&lt;/code&gt;, &lt;a href="https://github.com/filiphric" rel="noopener noreferrer"&gt;Filip Hric&lt;/a&gt;'s &lt;a href="https://github.com/NoriSte/ui-testing-best-practices/issues/43" rel="noopener noreferrer"&gt;cypress-plugin-steps&lt;/a&gt; is a valid alternative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use clear selectors
&lt;/h3&gt;

&lt;p&gt;Look at this code&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="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`{enter}{uparrow}&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;statements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createMutationGQLQuery&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="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="na"&gt;force&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="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is &lt;code&gt;cy.get('textarea').eq(0)&lt;/code&gt;? In the absence of better selectors, I suggest hiding them under Cypress aliases, such as&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;// Assign an alias to the most unclear selectors for future references&lt;/span&gt;
&lt;span class="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actionDefinitionTextarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&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;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typeConfigurationTextarea&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 then referring to them this way&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="nx"&gt;cy&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;@actionDefinitionTextarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clearConsoleTextarea&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to improve the readers' life.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing data-testid attributes
&lt;/h3&gt;

&lt;p&gt;I do not want to speak about the value for the tests themselves and for their confidence, but only about the effect of data-testid attributes over the debugging phase.&lt;/p&gt;

&lt;p&gt;If an element with a data-tesid cannot be retrieved from the page, the possible problems are&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The element is not there.&lt;/li&gt;
&lt;li&gt; The element is there, but it does not have the attribute.&lt;/li&gt;
&lt;li&gt; The element is there, and it has the attribute but not the expected value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All the above problems &lt;strong&gt;cause the developer to re-launch the tests, inspect the elements&lt;/strong&gt;, look for the test-related attributes, etc. Instead, if the tests are based on the textual contents, a screenshot is enough to understand if the text searched by the test is not there or if it is wrong.&lt;/p&gt;

&lt;p&gt;Also, some more cons for the engineers that have to deal with data-testid's&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Test-related attributes must be maintained during refactors, but it is not easy when you have hundreds of them.&lt;/li&gt;
&lt;li&gt; Test-related attributes are helpful if they are unique on the page. Another thing that is not easy to guarantee when you have hundreds of them&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My suggestion is to use data-testid attributes only for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Sections, not elements (ex. the Header, the Footer, , etc.) that allows reducing the scope of text-based searches. Here is an example
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cy&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;[data-test="Actions list"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;within&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- reduce the scope&lt;/span&gt;
&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- the "login" text could exist more times in the page&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt; Non-text-based elements: icons, images, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last but not least: I suggest valuing them with user-like values, not programmer-like ones (ex. "Actions List", not "actionsList"), especially when the section shows that exact text. This allows a direct connection between the code of the test, the Cypress' Test Runner, and the page's text content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grouping related actions
&lt;/h3&gt;

&lt;p&gt;Reading a flat list of interactions generally does not help comprehend the page's structure the test is running against.&lt;/p&gt;

&lt;p&gt;For instance&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  get 1 and click&lt;/li&gt;
&lt;li&gt;  get 2 and click&lt;/li&gt;
&lt;li&gt;  get 3 and click&lt;/li&gt;
&lt;li&gt;  get 4 and click&lt;/li&gt;
&lt;li&gt;  get 5 and click&lt;/li&gt;
&lt;li&gt;  get 6 and click&lt;/li&gt;
&lt;li&gt;  get 7 and click&lt;/li&gt;
&lt;li&gt;  get 8 and click&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, un-flattening the list could help the readers to create a mental model of where the involved parts reside&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  within block 1

&lt;ul&gt;
&lt;li&gt;  get 1 and click&lt;/li&gt;
&lt;li&gt;  get 2 and click&lt;/li&gt;
&lt;li&gt;  get 3 and click&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  within block 2

&lt;ul&gt;
&lt;li&gt;  get 4 and click&lt;/li&gt;
&lt;li&gt;  get 5 and click&lt;/li&gt;
&lt;li&gt;  get 6 and click&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  get 7 and click&lt;/li&gt;

&lt;li&gt;  get 8 and click&lt;/li&gt;

&lt;/ul&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%2Fm7wq7ny1bwv8zr65fzop.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%2Fm7wq7ny1bwv8zr65fzop.png" alt="The Cypress UI showing cy.within" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again: Storybook and Playwright already have the concept of &lt;code&gt;step&lt;/code&gt; utilities that allow grouping actions, the above suggestion is handy with Cypress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;p&gt;I touch code's readability in the following articles&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/from-unreadable-react-component-tests-to-simple-stupid-ones-3ge6"&gt;From unreadable React Component Tests to simple, stupid ones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/how-i-ease-the-next-developer-reading-my-code-1986"&gt;How I ease the next developer reading my code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/hasura-console-ui-coding-patterns-testing-281d"&gt;Hasura Console UI coding patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/routemanager-ui-coding-patterns-generic-ones-4iaa"&gt;WorkWave RouteManager UI coding patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and also I touch the topic of getting your code clear in the next ones&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/my-take-on-snapshot-testing-19k6"&gt;My take on Snapshot Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/how-i-strive-for-xstate-machine-types-and-tests-readability-19f4"&gt;How I strive for XState machine, types, and tests readability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/support-the-reviewers-with-detailed-pull-request-descriptions-2khn"&gt;Support the Reviewers with detailed Pull Request descriptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/re-building-a-branch-and-telling-a-story-to-ease-the-code-review-485o"&gt;Re-building a branch and telling a story to ease the Code Review&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last but not least: I loved and I suggest &lt;a href="https://www.oreilly.com/library/view/the-art-of/9781449318482/" rel="noopener noreferrer"&gt;"The Art of Readable Code" book&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>debug</category>
      <category>testing</category>
    </item>
    <item>
      <title>One long E2E test or small, independent ones?</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Mon, 06 Feb 2023 08:02:44 +0000</pubDate>
      <link>https://forem.com/noriste/one-long-e2e-test-or-small-independent-ones-33ao</link>
      <guid>https://forem.com/noriste/one-long-e2e-test-or-small-independent-ones-33ao</guid>
      <description>&lt;p&gt;While speaking about testing a CRUD app, how should we organize the "create", "modify", and "delete" E2E tests?&lt;/p&gt;

&lt;p&gt;The complete list of options are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; To have &lt;strong&gt;three small E2E tests dependent on the execution order&lt;/strong&gt; (test B takes for granted that test A run) - The only bad solution, I'm going to explain why.&lt;/li&gt;
&lt;li&gt; To have &lt;strong&gt;three small E2E tests independent from the execution order&lt;/strong&gt; (test B works regardless from whether test A was launched or not) - Theoretically, the best solution. Still, it requires a lot of boilerplates also to be fast.&lt;/li&gt;
&lt;li&gt; To have &lt;strong&gt;one extended E2E test&lt;/strong&gt; that does everything - A good tradeoff for the case presented in this article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@simonkadula?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Simon Kadula&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/8gr6bObQLOI?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It depends, and most of the problems I present are related to the implicit issues of the E2E tests, a strong signal that we should write only a few of them. As a Front-end Engineer, I strongly prefer to invest my time in server-free tests, not E2E ones. Go ahead, and you will understand why.&lt;/p&gt;

&lt;p&gt;Please note: this results from working on Hasura's Console E2E tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  1 - To have three small E2E tests dependent on the execution order (test B takes for granted that test A run)
&lt;/h2&gt;

&lt;p&gt;The test flow would be something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; START (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt; Test 1: create the entity&lt;/li&gt;
&lt;li&gt; Test 2: modify the entity&lt;/li&gt;
&lt;li&gt; Test 3: delete the entity&lt;/li&gt;
&lt;li&gt; END (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this case, the tests are not independent but based on execution order. To test a CRUD flow, the three primary tests are "create an entity", "modify an entity", "delete the entity". The second test ("modify the entity") takes for granted that when it starts, the application state is okay because it runs after the "create the entity" one. "delete the entity" must run after the "modify the entity" too, etc.&lt;/p&gt;

&lt;p&gt;Coupling multiple tests together is an anti-pattern because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;False negatives&lt;/strong&gt;: The tests will fail in a row once one fails.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Hard to debug&lt;/strong&gt;: understanding the root of a failure is more complicated because of higher ambiguity. Did the test fail because of its code? Or because the state of the previous test changed? Then, you have to debug two tests when one fails.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Hard to debug&lt;/strong&gt; (again): the developers waste a lot of time because they can not run a single test nor use &lt;code&gt;skip&lt;/code&gt; and &lt;code&gt;only&lt;/code&gt; to launch a portion of them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Hard to refactor&lt;/strong&gt;: The tests cannot be moved elsewhere. If the code of the tests becomes too long, too complex, etc., you cannot move it to a dedicated file/directory because it depends on the previous one.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Hard to read&lt;/strong&gt;: The readers cannot know what a test does because they must also know the previous tests. You have to read two tests instead of one, which is not good.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not recommend writing tests coupled this way, but I want to include them to be sure you realize why.&lt;/p&gt;

&lt;h2&gt;
  
  
  2 - To have three small E2E tests independent from the execution order
&lt;/h2&gt;

&lt;p&gt;To get every test independent, every test should create the application state that it needs to run, then clear it after completion. The flow presented in the previous chapter should become something like (in &lt;em&gt;italic&lt;/em&gt; the new steps compared to the original create-&amp;gt;modify-&amp;gt;delete three tests in a row)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; START (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt; Test 1: create the entity

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Load the page (the application state is empty)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; create the entity&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;AFTER&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Delete the *entity&lt;/em&gt; (the application state is empty)*&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; Test 2: modify the entity

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: create the *entity&lt;/em&gt; (through APIs)*&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Load the page (the application state is empty)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; modify the entity&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;AFTER&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Delete the *entity&lt;/em&gt; (through APIs, the application state is empty)*&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; Test 3: delete the entity

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: create the *entity&lt;/em&gt; (through APIs)*&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Load the page (the application state is empty)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; delete the entity&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;AFTER&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Delete the action (the application state is empty)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; END (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;By doing so, every test is independent. Please note that the before and after actions are done directly by calling the server APIs. Doing them through the UI would be too slow.&lt;/p&gt;

&lt;p&gt;Anyway, the presented approach's problem is that &lt;strong&gt;tests become slower&lt;/strong&gt; because every test creates the entity, and every test visits the page. When the application takes 10 seconds to load (it was initially the case of Hasura's Console), reloading the app is a problem.&lt;/p&gt;

&lt;p&gt;To get the best of both worlds (independent but fast tests), we should evolve the above flow to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Exploit the previous test's application state.&lt;/li&gt;
&lt;li&gt;  But also create the needed application state if no tests have run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Something like (in &lt;em&gt;italic&lt;/em&gt; the new steps compared to the flow presented in the previous chapter)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; START (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test 1: create the entity&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Does the *entity&lt;/em&gt; exist?*

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;NO: it's ok!&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;YES: delete the entity (through APIs)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; &lt;strong&gt;BEFORE&lt;/strong&gt;: Load the page (the application state is empty)&lt;/li&gt;

&lt;li&gt; create the entity&lt;/li&gt;

&lt;/ol&gt;

&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Test 2: modify the entity&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Does the *entity&lt;/em&gt; exist?*

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;YES: it's ok!&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;NO: create the entity (through APIs)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Does the &lt;em&gt;entity&lt;/em&gt; already includes the change the test is going to make?&lt;/em&gt;

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;YES: it's ok!&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;NO: modify the entity (through APIs)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Are we already on the correct page?&lt;/em&gt;

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;YES: it's ok!&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;NO: load the page&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; modify the entity&lt;/li&gt;

&lt;/ol&gt;

&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Test 3: delete the entity&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Does the entity exist?&lt;/em&gt;

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;YES: it's ok!&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;NO: create the entity (through APIs)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Are we already on the correct page?&lt;/em&gt;

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;YES: it's ok!&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;NO: load the page&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;/li&gt;

&lt;li&gt;&lt;p&gt;delete the entity&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;END&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;Now, if you run all the tests in a row, each of them leverages the existing application state. If you run just the "modify the entity" for instance, it creates whatever it needs, then runs the test itself.&lt;/p&gt;

&lt;p&gt;Now we have both test independence and test performance! Cool!&lt;/p&gt;

&lt;p&gt;Well... Did you notice the amount of code we need to write? The &lt;a href="https://github.com/bahmutov/cypress-data-session" rel="noopener noreferrer"&gt;cypress-data-session&lt;/a&gt; plugin comes in handy but&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; You have a lot of cypress-data-session related boilerplate&lt;/li&gt;
&lt;li&gt; You have to maintain, in the E2E tests, a lot of API calls that could go out of sync with the ones used in the main application in a while&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is an example of the cypress-data-session related boilerplate&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readMetadata&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;../services/readMetadata&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;deleteHakunaMatataPermission&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;../services/deleteHakunaMatataPermission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Ensure the Action does not have the Permission.
 *
 * ATTENTION: if you get the "setup function changed for session..." error, simply close the
 * Cypress-controlled browser and re-launch the test file.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hakunaMatataPermissionMustNotExist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;settingUpApplicationState&lt;/span&gt; &lt;span class="o"&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="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataSession&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;hakunaMatataPermissionMustNotExist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Without it, cy.dataSession run the setup function also the very first time, trying to&lt;/span&gt;
    &lt;span class="c1"&gt;// delete a Permission that does not exist&lt;/span&gt;
    &lt;span class="na"&gt;init&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the Permission exists&lt;/span&gt;
    &lt;span class="na"&gt;validate&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**--- Action check: start**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;readMetadata&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;loginAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&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="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="c1"&gt;// TODO: properly type it&lt;/span&gt;
          &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;loginAction&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;loginAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;permission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;loginAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;permission&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hakuna_matata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Returns true if the permission does not exist&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;permission&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="na"&gt;preSetup&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="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**--- The permission must be deleted**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;

    &lt;span class="c1"&gt;// Delete the Permission&lt;/span&gt;
    &lt;span class="na"&gt;setup&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;deleteHakunaMatataPermission&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;settingUpApplicationState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Ensure the UI read the latest data if it were previously loaded&lt;/span&gt;
        &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;and here is an example of the API call to create the entity&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="cm"&gt;/**
 * Create the Action straight on the server.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createLoginAction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**--- Action creation: start**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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;http://localhost:8080/v1/metadata&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bulk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// resource_version: 138,&lt;/span&gt;
    &lt;span class="na"&gt;args&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="na"&gt;type&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_custom_types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
          &lt;span class="na"&gt;input_objects&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="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;SampleInput&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;fields&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="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;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String!&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;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;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String!&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;span class="na"&gt;objects&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="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;SampleOutput&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;fields&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;accessToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String!&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="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;LoginResponse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;fields&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="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;accessToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;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;AddResult&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;fields&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;sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Int&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="na"&gt;enums&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;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;args&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;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;arguments&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="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;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="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;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;synchronous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;output_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LoginResponse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;handler&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://hasura-actions-demo.glitch.me/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mutation&lt;/span&gt;&lt;span class="dl"&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="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;request_transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**--- Action creation: end**&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;So, having independent tests is essential, but it comes with a cost.&lt;/p&gt;

&lt;p&gt;That's why, for this specific problem, I then opted for the last option...&lt;/p&gt;

&lt;h2&gt;
  
  
  3 - To have one extended E2E test that does everything
&lt;/h2&gt;

&lt;p&gt;Pros: a lot of boilerplate files can be removed.&lt;/p&gt;

&lt;p&gt;Cons: Working with the tests becomes slower (you cannot launch only the third test anymore)&lt;/p&gt;

&lt;p&gt;Compared to the boilerplate we need to write and the code we need to maintain, it is worth unifying them. After all, the specific CRUD flow I was working on took ~20 seconds.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; START (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt; Test: CRUD

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Delete the entity if it exists (the application state is empty)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;BEFORE&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Load the page&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; create the entity&lt;/li&gt;
&lt;li&gt; modify the entity&lt;/li&gt;
&lt;li&gt; delete the entity&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;em&gt;AFTER&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;: Delete the entity if it exists (the application state is empty)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; END (&lt;em&gt;the application state is empty&lt;/em&gt;)&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;And at the same time, it makes cypress-data-session useless. Hence one less dependency to keep updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Working with E2E tests is hard. Dealing with real data, real application state to clear, etc., has a cost. I know that E2E tests are the only ones that give complete confidence, but as a Front-end Engineer (remember, I'm not a QA Engineer), I strongly prefer to work with server-free tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;p&gt;If you found this article interesting, the following articles of mine could help you too&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/decouple-the-back-end-and-front-end-test-through-contract-testing-112k"&gt;Decouple the back-end and front-end test through Contract Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/speed-up-e2e-tests-for-vite-based-apps-3k4l"&gt;Speed up E2E tests for Vite-based apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/new-to-front-end-testing-start-from-the-top-of-the-pyramid-36kj"&gt;New to front-end testing? Start from the top of the pyramid!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/front-end-productivity-boost-cypress-as-your-main-development-browser-5cdk"&gt;Front-end productivity boost: Cypress as your main development browser&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>e2e</category>
    </item>
    <item>
      <title>Improving Hasura's Internal PR Review process</title>
      <dc:creator>Stefano Magni</dc:creator>
      <pubDate>Tue, 06 Dec 2022 09:52:56 +0000</pubDate>
      <link>https://forem.com/noriste/improving-hasuras-internal-pr-review-process-1ham</link>
      <guid>https://forem.com/noriste/improving-hasuras-internal-pr-review-process-1ham</guid>
      <description>&lt;p&gt;During 2022, thanks to &lt;a href="https://www.usehaystack.io/" rel="noopener noreferrer"&gt;Haystack&lt;/a&gt;, we realized our Pull Requests stayed open, on average, for seven days.&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%2Fgs8a8pvz7j3t1awmg1rj.jpg" 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%2Fgs8a8pvz7j3t1awmg1rj.jpg" title="The Haystack report" alt="Haystack reporting Hasura's PRs stay open for 171.3 hours on average!" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can imagine, this is troublesome because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It slows down the Software Development Life Cycle, preventing us from merging PRs and &lt;strong&gt;releasing new features for our users&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It requires a long back and forth between the PR author and the reviewers, sometimes resulting in the need to reload the context in people's minds again and again.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;PR’s code becomes outdated&lt;/strong&gt;, often requiring repeated merges/rebases and resolving conflicts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We all see the problem, but what causes it? Inspired by GitLab's model, we created a dedicated internal Working Group (inspired by &lt;a href="https://about.gitlab.com/company/team/structure/working-groups/" rel="noopener noreferrer"&gt;GitLab's Working Groups&lt;/a&gt;) to dig into the human aspects of this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Working Group proceeded
&lt;/h2&gt;

&lt;p&gt;Despite this being only the second internal Working Group, we already have a standard way to proceed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State the problem and explain why you think it's important&lt;/li&gt;
&lt;li&gt;Look for a sponsor among all the engineering managers&lt;/li&gt;
&lt;li&gt;Define the exit criteria&lt;/li&gt;
&lt;li&gt;Create a dedicated Slack channel&lt;/li&gt;
&lt;li&gt;Get people involved&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When speaking about the specific "PR Review Working Group", we decided to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Schedule some early meetings: people interact the most during sync meetings compared to async documents&lt;/li&gt;
&lt;li&gt;Elaborate on the problem and the possible solutions&lt;/li&gt;
&lt;li&gt;Request feedback&lt;/li&gt;
&lt;li&gt;Interview some internal engineers: we opted for interviews instead of sending out a survey&lt;/li&gt;
&lt;li&gt;Find some best practices&lt;/li&gt;
&lt;li&gt;Present the best practices internally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, &lt;a href="https://github.com/robertjdominguez" rel="noopener noreferrer"&gt;Rob Dominguez&lt;/a&gt;, &lt;a href="https://github.com/daniel-chambers" rel="noopener noreferrer"&gt;Daniel Chambers&lt;/a&gt;, and I (&lt;a href="https://github.com/NoriSte" rel="noopener noreferrer"&gt;Stefano Magni&lt;/a&gt;) started working on the Exit Criteria, drafted the first notes and the first opinionated solutions, asked for feedback, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exit criteria
&lt;/h3&gt;

&lt;p&gt;Defining the exit criteria of the Working Group is crucial to work toward a goal. We determined the following indicators:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identifying some best practices to follow to get PRs merged as soon as possible.&lt;/li&gt;
&lt;li&gt;Identifying the best practices in newly-opened PRs.&lt;/li&gt;
&lt;li&gt;Significantly shortening the average lifecycle of a PR.&lt;/li&gt;
&lt;li&gt;Improving the relationship between author and reviewer on both ends as measured by a monthly survey.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over the next several months, we'll monitor the progress and adoption of points 2 through 4. For the remainder of this article, we'll focus on how we identified these best practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root causes the Working Group found out
&lt;/h2&gt;

&lt;p&gt;We all know that a slow PR review process is terrible, but what are the root causes for this slowness? What happens between a developer creating the PR and getting the PR merged? Well, let’s list them in order of when they occur during the end-to-end PR creation and review process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Missing or not enough detailed PR description&lt;/strong&gt;: Reviewing a PR is mostly about understanding the author’s context and validating their changes against this context. When a description is missing or too short, it deprives the reviewers of context, making it more difficult to understand and adding a lot of drag to the whole process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Big PRs&lt;/strong&gt;: PR size directly impacts the review time. Working for a full sprint and then opening a single PR guarantees the PR will not reviewed promptly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code Owners&lt;/strong&gt;: &lt;a href="https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners" rel="noopener noreferrer"&gt;GitHub Code Owners&lt;/a&gt; help to define the specific people or groups that own part of the codebase. Until recently, PR reviews from code owners were mandatory, which prevents merging a PR quickly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lack of ownership&lt;/strong&gt;: When GitHub assigns reviews to a code owners group instead of an individual, nobody feels the review is their responsibility because they believe someone else will pick it up. An author won't waste time waiting for someone to review; they'll naturally move on to their next task, which, as we'll see later, causes them to lose their context on their work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Notifications missing&lt;/strong&gt;: The modern developer has so many services competing for their attention, and, in the deluge of notifications, it can be difficult to parse what is essential and what is simply noise. We determined that important messages notifying us of pending reviews or feedback on PRs went unnoticed too often.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PR review time not explicitly allocated&lt;/strong&gt;: Reviewing PRs takes time, primarily because of the complexity of Hasura's system. When planning the sprints, estimating features, etc., we must never forget to dedicate part of the day to reviewing each other people's PRs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timezone overlaps&lt;/strong&gt;: As a fully-distributed, remote-only company, Hasura's engineers work worldwide. Short overlap adds some delays when you need to interact with people on the opposite side of the world.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI issues&lt;/strong&gt;: CI is a gift and a curse, and it can make our review process more streamlined and give reviewers more confidence when used correctly. However, automated steps exceeded the scope of our WG; another group recently began work on this task in conjunction with our Infrastructure team to improve the developer experience.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The best practices
&lt;/h2&gt;

&lt;p&gt;One by one, let's check the best practices we identified for every mentioned problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  PR description missing or insufficiently detailed
&lt;/h3&gt;

&lt;p&gt;PR authors must try to walk in the reviewer's shoes. And the reviewers need to know a lot of things like&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What has changed?&lt;/li&gt;
&lt;li&gt;Why did you choose this solution? More importantly, why did you not select other solutions?&lt;/li&gt;
&lt;li&gt;What is the dependency graph of the changes? Where should I start reviewing the changes?&lt;/li&gt;
&lt;li&gt;How should I test/verify?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one word: context! The PR author must describe the PR context in detail, adding images, videos, &lt;a href="https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour" rel="noopener noreferrer"&gt;Code Tours&lt;/a&gt;, diagrams, pseudo-code; whatever helps reviewers understand the PR changes.&lt;/p&gt;

&lt;p&gt;It's important to understand that this activity could take one hour or more, but &lt;strong&gt;every minute dedicated to describing the PR saves every reviewer two minutes&lt;/strong&gt;. Also, the description acts as documentation for posterity and could help the PR authors when they have to return to the PR after some time.&lt;/p&gt;

&lt;p&gt;Thanks to a detailed description, reviewers can dedicate their time by checking that the developers’ code reflects their intentions instead of guessing what was in the developers’ minds when they wrote the code.&lt;/p&gt;

&lt;p&gt;Tests are also an essential part of the PR; they should be present and reflect the changes introduced by the PR. Tests serve as a quick summary answering the question,  “What should the code do?”&lt;/p&gt;

&lt;h3&gt;
  
  
  Big PRs
&lt;/h3&gt;

&lt;p&gt;Authors need to create reasonably-sized PRs. All projects - or even subsets of codebases - are different, but the size of a PR exponentially impacts the time required to review them. &lt;strong&gt;The best way to create easily-parsed PRs is... writing a detailed description!&lt;/strong&gt; Why? Because if you are describing your PR in detail, and you realize that you are spending hours describing it, chances are the PR is too big.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Owners
&lt;/h3&gt;

&lt;p&gt;Before August 2022, we organized Hasura into stack-oriented teams (the Server team, the Console team, etc.), and code-owners groups reflected this internal structure. Since this created too many cross-team dependencies (resulting in slowing down E2E features implementation), we reorganized into feature-focused teams that include all the necessary people to implement and release features independently.&lt;/p&gt;

&lt;p&gt;As a result, if GitHub automatically assigns your PR to the Console code-owners group, there's a high probability that you have at least a couple of Console developers on your team! And since they participate in your standup and align with the team goals, they are the best candidates to review your Console-related PR.&lt;/p&gt;

&lt;p&gt;The suggestion is always to use Code Owners as a hint to understand what knowledge people should have to review your PR, then find the people with that knowledge in your team and assign the PR to them.&lt;/p&gt;

&lt;p&gt;In the future, code owners at Hasura will reflect the new teams’ structure, but this is only partially possible at the time of writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lack of ownership
&lt;/h3&gt;

&lt;p&gt;Lack of ownership happens in two different  moments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When no one reviews a PR&lt;/li&gt;
&lt;li&gt;When the PR authors forget about their PR&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We can automatically resolve the former case with what we shared in the previous point via assigning a specific person to review a PR. In contrast, the latter happens when the PR authors become tired of waiting for a review and start working on new tasks.&lt;/p&gt;

&lt;p&gt;Whenever possible - aka, when getting the PR merged is a matter of hours - the PR authors should &lt;strong&gt;not&lt;/strong&gt; start working on new tasks, but they should opt for getting the existing PR merged. Working on new tasks means forgetting and de-prioritizing the current PR, while striving to get it merged to ensure it will bring to completion.&lt;/p&gt;

&lt;p&gt;What happens when a review takes too long? Simply put: authors get stressed. However, this response is critical to get them to become self-advocates and draw attention to the issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raise their hands.&lt;/li&gt;
&lt;li&gt;Send messages.&lt;/li&gt;
&lt;li&gt;Ask for help.&lt;/li&gt;
&lt;li&gt;Report the problem to the managers and in the retros.&lt;/li&gt;
&lt;li&gt;Escalate if the problem persists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We know this process could be better, but it also works towards resolving the issue. Working on new tasks, instead, buries the problem under the rug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notifications missing
&lt;/h3&gt;

&lt;p&gt;We solved this problem by:&lt;/p&gt;

&lt;p&gt;Firstly, enabling GitHub real-time alerts since it's the best way to receive only the important notifications in Slack&lt;br&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%2Fep9xedqm7esxswit91ub.jpg" 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%2Fep9xedqm7esxswit91ub.jpg" title="GitHub real-time alerts for Slack" alt="GitHub showing all the options for Slack real-time alerts" width="800" height="1012"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Secondly, we enabled &lt;a href="https://www.usehaystack.io/" rel="noopener noreferrer"&gt;Haystack&lt;/a&gt; daily reports in Slack, so that teams never forget their waiting, stale or large PRs.&lt;br&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%2Famyy8zl9xrmjnyrrtvpc.jpg" 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%2Famyy8zl9xrmjnyrrtvpc.jpg" title="GitHub real-time alerts for Slack" alt="GitHub real-time alerts for Slack" width="800" height="1080"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  PR review time not explicitly allocated
&lt;/h3&gt;

&lt;p&gt;We suggested that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Individuals should enable GitHub real-time alerts, since it is the best way to receive only important notifications in Slack.&lt;/li&gt;
&lt;li&gt;Teams should prioritize reviewing PRs (who cares if a team creates a lot of PRs without getting them merged?)&lt;/li&gt;
&lt;li&gt;Teams should raise the problem of lack of review time during retros&lt;/li&gt;
&lt;li&gt;Teams should set correct expectations in terms of workload the team can afford, also considering the code reviews&lt;/li&gt;
&lt;li&gt;Team managers always remember that soft code reviews often result in bugs hitting our users in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Timezone overlaps
&lt;/h3&gt;

&lt;p&gt;We reduced this problem almost to zero naturally by assigning PR reviews to peers from the same team instead of code owners. We built feature teams with timezone compatibility in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens if we do not follow the above best practices?
&lt;/h2&gt;

&lt;p&gt;Simply put:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We start working on many features, but we release only a few.&lt;/li&gt;
&lt;li&gt;We release features at a slower pace.&lt;/li&gt;
&lt;li&gt;We end up with a backlog of stale PRs that haven't seen activity in months and wither away, neglected on GitHub's servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The presentation
&lt;/h2&gt;

&lt;p&gt;Instead of opting for the standard slide deck, we made a more engaging presentation focused on a story: a Hasura developer opening a PR and facing all the PR review-related problems. We wanted attendees to garner empathy - a company core value - for devs on either side of this process.&lt;/p&gt;

&lt;p&gt;To make the story more engaging, we relied heavily on gifs and exaggerated - only slightly! - some of the impediments we all face in this industry.&lt;/p&gt;

&lt;p&gt;We created some vertical slides to have the slides side-by-side with the presenter (using a virtual camera and OBS, you can find all the instructions in &lt;a href="https://twitter.com/NoriSte/status/1354801517178437633" rel="noopener noreferrer"&gt;this tweet&lt;/a&gt; about how to do the same).&lt;/p&gt;

&lt;p&gt;Here is a picture of the presentation layout:&lt;br&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%2Fa4wlswkslg1q96tnmrb0.jpg" 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%2Fa4wlswkslg1q96tnmrb0.jpg" title="A picture taken before the presentation" alt="The presenter side-by-side with the vertical slides of the presentation" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Once we identified the best practices, we immediately put the "Starting to review PRs" topic in our onboarding docs to ensure new hires understand the importance of these best practices within their first few weeks.&lt;/p&gt;

&lt;p&gt;Over the next few months, we'll continue analyzing metrics via Haystack to understand if the average number of days a PR stays open decreases. If not, we will try to understand the developer's difficulties applying the best practices mentioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special thanks
&lt;/h2&gt;

&lt;p&gt;I (&lt;a href="https://github.com/NoriSte" rel="noopener noreferrer"&gt;Stefano Magni&lt;/a&gt;) volunteered to create and drive the Working Group, but I want to thank &lt;a href="https://github.com/robertjdominguez" rel="noopener noreferrer"&gt;Rob Dominguez&lt;/a&gt; for raising the quality of our work by doing a lot of under-the-hood things. His presence and energy always motivated me too!&lt;/p&gt;

&lt;p&gt;Thanks a lot also to &lt;a href="https://github.com/daniel-chambers" rel="noopener noreferrer"&gt;Daniel Chambers&lt;/a&gt;. Daniel got involved in all the discussions, provided a lot of feedback, and volunteered to interview some of our developers. Thanks, Daniel!&lt;/p&gt;

&lt;p&gt;Finally, thanks to all the people we interviewed and those who participated in every async and sync discussion ❤️.&lt;/p&gt;

&lt;h2&gt;
  
  
  External resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/support-the-reviewers-with-detailed-pull-request-descriptions-2khn"&gt;Stefano Magni's Support the Reviewers with detailed Pull Request descriptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/re-building-a-branch-and-telling-a-story-to-ease-the-code-review-485o"&gt;Stefano Magni's Re-building a branch and telling a story to ease the Code Review&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/noriste/a-case-history-analysing-hasura-consoles-code-review-process-45kk"&gt;Stefano Magni's A Case History: Analysing Hasura Console's code review process&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thinkinglabs.io/articles/2022/09/17/the-practices-that-make-continuous-integration-team-working.html" rel="noopener noreferrer"&gt;Thierry de Pauw's The Practices That Make Continuous Integration - Team Working for Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thinkinglabs.io/articles/2022/09/25/the-practices-that-make-continuous-integration-coding.html" rel="noopener noreferrer"&gt;Thierry de Pauw's The Practices That Make Continuous Integration - Coding for Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thinkinglabs.io/articles/2022/09/28/the-practices-that-make-continuous-integration-building.html" rel="noopener noreferrer"&gt;Thierry de Pauw's The Practices That Make Continuous Integration - Building for Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/eng-practices/review/" rel="noopener noreferrer"&gt;Google's Code Review Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://conventionalcomments.org/" rel="noopener noreferrer"&gt;Conventional Comments&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
