<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ monitoring - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ monitoring - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 23 May 2026 16:27:15 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/monitoring/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Self-Host Your Own Server Monitoring Dashboard Using Uptime Kuma and Docker ]]>
                </title>
                <description>
                    <![CDATA[ As a developer, there's nothing worse than finding out from an angry user that your website is down. Usually, you don't know your server crashed until someone complains. And while many SaaS tools can  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/self-host-uptime-kuma-docker/</link>
                <guid isPermaLink="false">69d4185f40c9cabf44851652</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-hosted ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Ubuntu ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdul Talha ]]>
                </dc:creator>
                <pubDate>Mon, 06 Apr 2026 20:32:31 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ea068a20-bc19-400a-a42e-1bbb7e492da8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a developer, there's nothing worse than finding out from an angry user that your website is down. Usually, you don't know your server crashed until someone complains.</p>
<p>And while many SaaS tools can monitor your site, they often charge high monthly fees for simple alerts.</p>
<p>My goal with this article is to help you stop paying those expensive fees by showing you a powerful, free, open-source alternative called Uptime Kuma.</p>
<p>In this guide, you'll learn how to use Docker to deploy Uptime Kuma safely on a local Ubuntu machine.</p>
<p>By the end of this tutorial, you'll have set up your own private server monitoring dashboard in less than 10 minutes and created an automated Discord alert to ping your phone if your website goes offline.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-step-1-update-packages-and-prepare-the-firewall">Step 1: Update Packages and Prepare the Firewall</a></p>
</li>
<li><p><a href="#heading-step-2-create-the-docker-compose-file">Step 2: Create the Docker Compose File</a></p>
</li>
<li><p><a href="#heading-step-3-start-the-application">Step 3: Start the Application</a></p>
</li>
<li><p><a href="#heading-step-4-access-the-dashboard">Step 4: Access the Dashboard</a></p>
</li>
<li><p><a href="#heading-step-5-use-case-monitor-a-website-and-send-discord-alerts">Step 5: Use Case – Monitor a Website and Send Discord Alerts</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>An Ubuntu machine (like a local server, VM, or desktop).</p>
</li>
<li><p>Docker and Docker Compose installed.</p>
</li>
<li><p>Basic knowledge of the Linux terminal.</p>
</li>
</ul>
<h2 id="heading-step-1-update-packages-and-prepare-the-firewall">Step 1: Update Packages and Prepare the Firewall</h2>
<p>First, you'll want to make sure your system has the newest updates. Then, you'll install the Uncomplicated Firewall (UFW) and open the network "door" (port) that Uptime Kuma uses for the dashboard. You'll also need to allow SSH so you don't lock yourself out.</p>
<p>Run these commands in your terminal:</p>
<ol>
<li>Update your packages:</li>
</ol>
<pre><code class="language-shell">sudo apt update &amp;&amp; sudo apt upgrade -y
</code></pre>
<ol>
<li>Install the firewall:</li>
</ol>
<pre><code class="language-shell">sudo apt install ufw -y
</code></pre>
<ol>
<li>Allow SSH and open port 3001:</li>
</ol>
<pre><code class="language-shell">sudo ufw allow ssh
sudo ufw allow 3001/tcp
</code></pre>
<ol>
<li>Enable the firewall:</li>
</ol>
<pre><code class="language-shell">sudo ufw enable
sudo ufw reload
</code></pre>
<h2 id="heading-step-2-create-the-docker-compose-file">Step 2: Create the Docker Compose File</h2>
<p>Using a <code>docker-compose.yml</code> file is the professional way to manage Docker containers. It keeps your setup organised in one single place.</p>
<p>To start, create a new folder for your project and enter it:</p>
<pre><code class="language-shell">mkdir uptime-kuma &amp;&amp; cd uptime-kuma
</code></pre>
<p>Then create the configuration file:</p>
<pre><code class="language-shell">nano docker-compose.yml
</code></pre>
<p>Paste the following code into the editor:</p>
<pre><code class="language-yaml">services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    ports:
      - "3001:3001"
</code></pre>
<p><strong>Note</strong>: The <code>./data:/app/data</code> line is very important. It saves your database in a normal folder on your machine, making it easy to back up later.</p>
<p>Finally, save and exit: Press <code>CTRL + X</code>, then <code>Y</code>, then <code>Enter</code>.</p>
<h2 id="heading-step-3-start-the-application">Step 3: Start the Application</h2>
<p>Now, tell Docker to read your file and start the monitoring service in the background.</p>
<pre><code class="language-shell">docker compose up -d
</code></pre>
<p><strong>How to verify:</strong> Docker will download the files. When it finishes, your terminal should print <code>Started uptime-kuma</code>.</p>
<h2 id="heading-step-4-access-the-dashboard">Step 4: Access the Dashboard</h2>
<p>To access the dashboard, first open your web browser and go to <code>http://localhost:3001</code> (or your machine's local IP address).</p>
<p>When asked to choose the database, select <strong>SQLite</strong>. It's simple, fast, and requires no extra setup.</p>
<p>Then create an account and choose a secure admin username and password.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/02913589-020e-4a8a-aa7a-1bf70a9244c6.png" alt="02913589-020e-4a8a-aa7a-1bf70a9244c6" style="display:block;margin:0 auto" width="908" height="851" loading="lazy">

<h2 id="heading-step-5-use-case-monitor-a-website-and-send-discord-alerts">Step 5: Use Case – Monitor a Website and Send Discord Alerts</h2>
<p>Now you'll put Uptime Kuma to work by monitoring a live website and setting up an alert. Just follow these steps:</p>
<ol>
<li><p>Click Add New Monitor.</p>
</li>
<li><p>Set the Monitor Type to <code>HTTP(s)</code>.</p>
</li>
<li><p>Give it a Friendly Name (e.g., "My Blog") and enter your website's URL.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/74567f1e-acc4-480f-b969-7883e01aa459.png" alt="74567f1e-acc4-480f-b969-7883e01aa459" style="display:block;margin:0 auto" width="1918" height="867" loading="lazy">

<h3 id="heading-pro-tip-how-to-fix-down-errors-bot-protection">Pro-Tip: How to Fix "Down" Errors (Bot Protection)</h3>
<p>If your site uses strict security, it might block Uptime Kuma and say your site is "Down" with a 403 Forbidden error.</p>
<p><strong>The Fix:</strong> Scroll down to Advanced, find the User Agent box, and paste this text to make Uptime Kuma look like a normal Chrome browser:</p>
<p><code>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36</code></p>
<h3 id="heading-add-a-discord-alert">Add a Discord Alert</h3>
<p>To get a message on your phone when your site goes down:</p>
<ol>
<li><p>On the right side of the monitor screen, click Setup Notification.</p>
</li>
<li><p>Select Discord from the dropdown list.</p>
</li>
<li><p>Paste a Discord Webhook URL (you can create one in your Discord server settings under Integrations).</p>
</li>
<li><p>Click Test to receive a test ping, then click Save.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations! You just took control of your server health. By deploying Uptime Kuma, you replaced an expensive SaaS subscription with a powerful, free monitoring tool that alerts you the second a project goes offline.</p>
<p><strong>Let’s connect!</strong> I am a developer and technical writer specialising in writing step-by-step guides and workflows. You can find my latest projects on my <a href="https://blog.abdultalha.tech/portfolio"><strong>Technical Writing Portfolio</strong></a> or reach out to me directly on <a href="https://www.linkedin.com/in/abdul-talha/"><strong>LinkedIn</strong></a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Top Application Monitoring Tools for Developers ]]>
                </title>
                <description>
                    <![CDATA[ If your app runs in production, you’ll need to know when it breaks. Preferably before your users tell you. That’s where application monitoring tools (APM) come in. They show you what’s working, what’s slow, and what’s failing, all in one place. Here ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/top-application-monitoring-tools-for-developers/</link>
                <guid isPermaLink="false">6865cf231aa55afae259e821</guid>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Thu, 03 Jul 2025 00:30:27 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751502612871/600e6862-d4e5-42f3-a1eb-2cc0d618cc4c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If your app runs in production, you’ll need to know when it breaks. Preferably before your users tell you.</p>
<p>That’s where application monitoring tools (APM) come in. They show you what’s working, what’s slow, and what’s failing, all in one place.</p>
<p>Here are five of the best tools developers use today. I’ll walk you through what they do, why they’re good, and how you might use them in your projects.</p>
<h2 id="heading-new-relichttpsnewreliccom"><a target="_blank" href="https://newrelic.com/"><strong>New Relic</strong></a></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751446743998/147dbd77-8cb5-455a-a5cc-4e85bacc7f36.png" alt="New relic dashboard" class="image--center mx-auto" width="2400" height="1381" loading="lazy"></p>
<p>New Relic is one of the oldest players in this space. It’s great for full-stack observability, meaning you can see everything from your frontend JavaScript errors to database query times.</p>
<p>Imagine your <a target="_blank" href="https://www.freecodecamp.org/news/what-exactly-is-node-guide-for-beginners/">Node.js backend</a> is running slow. You deploy a new endpoint, and your API response times go up.</p>
<p>With New Relic, you can trace that slowdown to a specific function call or database query. It shows you performance metrics, transaction traces, error rates, and alerts in real time.</p>
<p>For beginners, New Relic’s dashboard can feel overwhelming. But once you get used to it, you’ll see why large teams rely on it for 24/7 monitoring.</p>
<p>If you want one tool that does application performance monitoring (APM), infrastructure monitoring, browser monitoring, and even mobile monitoring in one place, New Relic is your tool.</p>
<p>New Relic is a paid tool, but it comes with a generous free tier for you to start exploring its features. <a target="_blank" href="https://newrelic.com/pricing">Here is the full pricing plan</a>.</p>
<h2 id="heading-datadoghttpswwwdatadoghqcom"><a target="_blank" href="https://www.datadoghq.com/"><strong>Datadog</strong></a></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751446766051/fada608d-08f6-46bb-b2fd-fc9f5c12dff0.png" alt="Datadog dashboard" class="image--center mx-auto" width="2368" height="1314" loading="lazy"></p>
<p>Datadog started as an infrastructure monitoring tool but has become a powerhouse for developers, too.</p>
<p>It integrates easily with AWS, Azure, GCP, Kubernetes, Docker, and almost any service you use.</p>
<p>Assume you deploy a Flask app on Kubernetes. Suddenly, users report timeouts.</p>
<p>In Datadog, you can view your pod metrics, CPU and memory usage, container logs, and APM traces all in one timeline. You’ll quickly see if your pod was <a target="_blank" href="https://komodor.com/learn/how-to-fix-oomkilled-exit-code-137/">OOMKilled</a>, if your database had connection spikes, or if your app code itself was slow.</p>
<p>Datadog also shines in alerting. You can set up alerts like:</p>
<p>If average response time &gt; 2000ms for 5 minutes, send Slack alert to #devops</p>
<p>This keeps your team proactive instead of reactive.</p>
<p>It also integrates <a target="_blank" href="https://poweradspy.com/behavioral-targeting-working-and-benefits/">behavioural targeting</a> data from user sessions and performance metrics, helping product teams understand how performance issues affect user behaviour and conversion.</p>
<p>If you want seamless cloud-native monitoring with powerful dashboards, alerts, and security integrations, Datadog is your solution.</p>
<p>Datadog is free for up to five hosts, so the free plan would be sufficient for solo developers / small teams to get started. <a target="_blank" href="https://www.datadoghq.com/pricing/">Here is the full pricing plan</a>.</p>
<h2 id="heading-prometheus-grafanahttpsprometheusio"><a target="_blank" href="https://prometheus.io/"><strong>Prometheus + Grafana</strong></a></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751446780239/9f296c52-7467-4693-b0f3-bcc1cd2024dc.png" alt="Graphana Dashboard with Prometheus Data" class="image--center mx-auto" width="1266" height="684" loading="lazy"></p>
<p>Prometheus is an open-source monitoring system that scrapes metrics from your app, stores them in a time-series database, and lets you query them with PromQL.</p>
<p><a target="_blank" href="https://grafana.com/">Grafana</a> is the dashboard layer on top. Together, they’re like peanut butter and jelly for monitoring.</p>
<p>Here’s how you can use them. Suppose you have a Go API exposing metrics on /metrics using the Prometheus client library. Prometheus scrapes that endpoint every 15 seconds. You can query:</p>
<p><code>rate(http_requests_total[5m])</code></p>
<p>This shows you the average requests per second over the last 5 minutes.</p>
<p>Then, in Grafana, you build dashboards to visualise that data with graphs, gauges, and alerts. Many teams use Grafana for system health dashboards displayed on TVs in the office.</p>
<p>Prometheus is free, flexible, and used heavily with Kubernetes because of its service discovery features. But it requires setup and maintenance compared to SaaS tools.</p>
<p>If you want a powerful open-source solution with custom dashboards and PromQL querying, Prometheus + Graphana is your solution.</p>
<h2 id="heading-sentryhttpssentryio"><a target="_blank" href="https://sentry.io/"><strong>Sentry</strong></a></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751446791539/0eef1f85-fc50-4cee-8930-05cbdc99b772.png" alt="Sentry Dashboard" class="image--center mx-auto" width="1220" height="900" loading="lazy"></p>
<p>Unlike New Relic and Datadog, Sentry focuses on error and performance monitoring.</p>
<p>It’s a favourite for frontend and backend developers because it gives detailed stack traces, breadcrumbs, and <a target="_blank" href="https://support.atlassian.com/organization-administration/docs/what-are-release-tracks/">release tracking</a>.</p>
<p>For example, say your React app throws an error when users click “Submit”. Sentry captures:</p>
<ul>
<li><p>The exact error and message</p>
</li>
<li><p>The function and file that caused it</p>
</li>
<li><p>The user’s browser and OS</p>
</li>
<li><p>The recent events (breadcrumbs) before the error</p>
</li>
</ul>
<p>This helps you reproduce and fix the issue fast.</p>
<p>On backend apps, it works similarly to the frontend. You can integrate Sentry with Django, Express, Flask, or almost any framework to capture exceptions and performance bottlenecks.</p>
<p>If you want to track bugs and performance issues in real time, with deep context to debug them quickly, Sentry is your solution.</p>
<p>Sentry is free for a single user with minimal features. <a target="_blank" href="https://sentry.io/pricing/">Here is the full pricing plan</a>.</p>
<h2 id="heading-posthoghttpsposthogcom"><a target="_blank" href="https://posthog.com/"><strong>PostHog</strong></a></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751446803180/eeaff4a6-7d4d-4420-ae46-bfe1c50c584b.png" alt="Posthog Dashboard" class="image--center mx-auto" width="1200" height="959" loading="lazy"></p>
<p>PostHog is a modern open-source tool for product analytics, session recording, feature flags, and application monitoring.</p>
<p>Unlike traditional APM tools, PostHog focuses on understanding how users interact with your app.</p>
<p>For example, suppose users aren’t completing your signup flow. With PostHog, you can:</p>
<ul>
<li><p>Record user sessions to see exactly where they drop off</p>
</li>
<li><p>Track funnel conversion rates step by step</p>
</li>
<li><p>Analyse feature usage to prioritise improvements</p>
</li>
<li><p>Use behavioural targeting to trigger in-app prompts for specific user segments</p>
</li>
</ul>
<p>You can self-host PostHog on your infrastructure or use their cloud offering. Developers like it because it combines product analytics and user insights without sending data to third parties if self-hosted.</p>
<p>If you want to combine product analytics, session replays, feature flags, and event-based monitoring in one tool to understand and improve user behaviour in your app, Posthog is your solution.</p>
<p>PostHog has a generous free tier for up to 1 million events per month. Paid plans start from $0.00045 per event after the free tier, with enterprise features and advanced plugins. So there is no fixed pricing and you pay as your application scales.</p>
<h2 id="heading-so-which-apm-tool-should-you-pick"><strong>So which APM tool should you pick?</strong></h2>
<p>If you’re a solo developer or a small team, start with Sentry for errors and Prometheus + Grafana for open-source metrics.</p>
<p>As you grow and need unified monitoring with alerts and APM, tools like Datadog or New Relic become valuable.</p>
<p>If you want full control of your data with modern APM features and pricing that scales with your app, Posthog is your solution.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Remember, monitoring isn’t just about fixing failures. It’s about learning how your app behaves under real usage. This helps you optimise performance, spot bottlenecks, and build resilient software that users trust.</p>
<p>Take some time to integrate at least basic monitoring into your apps. Even simple HTTP request metrics and error alerts can save you hours of blind debugging later.</p>
<p>Hope you found this article useful. <a target="_blank" href="https://www.linkedin.com/in/manishmshiva/">Get in touch with me</a> on LinkedIn.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Front-End Monitoring Handbook: Track Performance, Errors, and User Behavior ]]>
                </title>
                <description>
                    <![CDATA[ A complete frontend monitoring system is essential for tracking application performance, errors, and user behavior. It consists of three main components: data collection and reporting, data processing and storage, and data visualization. This article... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-front-end-monitoring-handbook/</link>
                <guid isPermaLink="false">683e5606b7c8b1ebd3a32964</guid>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ frontend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gordan Tan ]]>
                </dc:creator>
                <pubDate>Tue, 03 Jun 2025 01:55:18 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748915696356/6e6edeed-2f41-40f9-97d6-8cc8686c3b25.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A complete frontend monitoring system is essential for tracking application performance, errors, and user behavior. It consists of three main components: data collection and reporting, data processing and storage, and data visualization.</p>
<p>This article focuses specifically on the first component – data collection and reporting – and shows you how to build a monitoring SDK from scratch. By the end of this article, you'll understand how to gather critical metrics about your application's performance, capture errors, track user behavior, and implement efficient reporting mechanisms.</p>
<p>Below is an outline of the topics we'll cover:</p>
<pre><code class="lang-javascript">                       ┌────────────────────┐
                       │  Data Collection   │
                       └──────────┬─────────┘
                                  │
         ┌─────────────────┬──────┴──────────────┐
         │                 │                     │
┌────────┴────────┐ ┌──────┴──────┐     ┌────────┴────────┐
│ <span class="hljs-built_in">Error</span> Monitoring │ │ Performance  │     │ Behavior       │
│                  │ │ Monitoring   │     │ Monitoring     │
└────────┬─────────┘ └──────┬──────┘     └────────┬────────┘
         │                  │                     │
┌────────┴────────┐ ┌──────┴──────────┐  ┌────────┴────────┐
│                 │ │                 │  │                 │
│ Resource Loading│ │ Resource Loading│  │     UV, PV      │
│     Errors      │ │      Time       │  │                 │
│                 │ │                 │  │  Page Access    │
│   JS Errors     │ │  API Request    │  │     Depth       │
│                 │ │     Time        │  │                 │
│ <span class="hljs-built_in">Promise</span> Errors  │ │                 │  │   Page Stay     │
│                 │ │   DNS, TCP,     │  │    Duration     │
│ Custom Errors   │ │ First-byte Time │  │                 │
│                 │ │                 │  │  Custom Event   │
│                 │ │   FPS Rate      │  │    Tracking     │
│                 │ │                 │  │                 │
│                 │ │ Cache Hit Rate  │  │   User Clicks   │
│                 │ │                 │  │                 │
│                 │ │  First Screen   │  │ Page Navigation │
│                 │ │  Render Time    │  │                 │
│                 │ │                 │  └─────────────────┘
│                 │ │  FP, FCP, LCP,  │
│                 │ │   FID, LCS,     │
│                 │ │ DOMContentLoaded│
│                 │ │    onload       │
└─────────────────┘ └─────────────────┘
</code></pre>
<p>Once data is collected, it needs to be reported to your backend systems for processing and analysis:</p>
<pre><code class="lang-javascript">                  ┌─────────────────┐
                  │ Data Reporting  │
                  └────────┬────────┘
                           │
          ┌────────────────┴────────────────┐
          │                                 │
┌─────────────────────┐           ┌─────────────────────┐
│  Reporting Methods  │           │  Reporting Timing   │
└──────────┬──────────┘           └──────────┬──────────┘
           │                                 │
     ┌─────┼─────┐               ┌───────────┼───────────┐
     │     │     │               │           │           │
┌────┴───┐ │ ┌───┴────┐ ┌────────┴────────┐ │ ┌─────────┴─────────┐
│  xhr   │ │ │ image  │ │ requestIdle     │ │ │ Upload when cache │
└────────┘ │ └────────┘ │ Callback/       │ │ │ limit is reached  │
           │            │ <span class="hljs-built_in">setTimeout</span>      │ │ └───────────────────┘
     ┌─────┴─────┐      └─────────────────┘ │
     │ sendBeacon│                          │
     └───────────┘                ┌─────────┴──────────┐
                                  │    beforeunload    │
                                  └────────────────────┘
</code></pre>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before diving into this tutorial, you should have:</p>
<ul>
<li><p>Basic knowledge of JavaScript and web development</p>
</li>
<li><p>Familiarity with browser APIs and event handling</p>
</li>
<li><p>Understanding of asynchronous programming concepts</p>
</li>
<li><p>Some experience with performance optimization concepts</p>
</li>
</ul>
<p>Since theoretical knowledge alone can be difficult to grasp, I've created a simple <a target="_blank" href="https://github.com/woai3c/monitor-demo">monitoring SDK</a> that implements these technical concepts. You can use it to create simple demos and gain a better understanding. Reading this article while experimenting with the SDK will provide the best learning experience.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-collect-performance-data">Collect Performance Data</a></p>
<ul>
<li><p><a class="post-section-overview" href="##heading-fp-first-paint">FP (First Paint)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fcp-first-contentful-paint">FCP (First Contentful Paint)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lcp-largest-contentful-paint">LCP (Largest Contentful Paint)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cls-cumulative-layout-shift">CLS (Cumulative Layout Shift)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-domcontentloaded-and-load-events">DOMContentLoaded and Load Events</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-first-screen-rendering-time">First Screen Rendering Time</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-api-request-timing">API Request Timing</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-resource-loading-time-and-cache-hit-rate">Resource Loading Time and Cache Hit Rate</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-browser-backforward-cache-bfc">Browser Back/Forward Cache</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fps">FPS</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vue-router-change-rendering-time">Vue Router Change Rendering Time</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-error-data-collection">Error Data Collection</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-resource-loading-errors">Resource Loading Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-javascript-errors">JavaScript Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-promise-errors">Promise Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sourcemap">Sourcemap</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vue-errors">Vue Errors</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-behavior-data-collection">Behavior Data Collection</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-pv-and-uv">PV and UV</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-page-stay-duration">Page Stay Duration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-page-access-depth">Page Access Depth</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-user-clicks">User Clicks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-page-navigation">Page Navigation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vue-router-changes">Vue Router Changes</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-data-reporting">Data Reporting</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-reporting-methods">Reporting Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reporting-timing">Reporting Timing</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-performance-monitoring">Performance Monitoring</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-error-monitoring">Error Monitoring</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-behavior-monitoring">Behavior Monitoring</a></p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-collect-performance-data">Collect Performance Data</h2>
<p>Monitoring performance is crucial for providing users with a smooth, responsive experience. Slow websites lead to higher bounce rates and reduced conversions. By collecting performance metrics, you can identify bottlenecks, optimize critical rendering paths, and improve overall user satisfaction.</p>
<p>The Chrome developer team has proposed a series of metrics to monitor page performance, each measuring a different aspect of the user experience:</p>
<ul>
<li><p><strong>FP (First Paint)</strong> – Time from when the page starts loading until the first pixel is painted on the screen (essentially the white screen time)</p>
</li>
<li><p><strong>FCP (First Contentful Paint)</strong> – Time from page load start until any part of page content is rendered</p>
</li>
<li><p><strong>LCP (Largest Contentful Paint)</strong> – Time from page load start until the largest text block or image element completes rendering</p>
</li>
<li><p><strong>CLS (Cumulative Layout Shift)</strong> – Cumulative score of all unexpected layout shifts occurring between page load start and when the <a target="_blank" href="https://developer.chrome.com/docs/web-platform/page-lifecycle-api">page's lifecycle state</a> becomes hidden</p>
</li>
</ul>
<p>We can obtain these four performance metrics through <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver">PerformanceObserver</a> (they can also be retrieved via <code>performance.getEntriesByName()</code>, but this method doesn't provide real-time notifications when events occur). PerformanceObserver is a performance monitoring interface used to observe performance measurement events.</p>
<p>Let's examine each of these metrics in detail and see how to implement them in our SDK.</p>
<h3 id="heading-fp-first-paint">FP (First Paint)</h3>
<p>First Paint (FP) marks the point when the browser renders anything visually different from what was on the screen before navigation. This could be a background color change or any visual element that indicates to the user that the page is loading.</p>
<p>Implementation code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {        
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-keyword">if</span> (entry.name === <span class="hljs-string">'first-paint'</span>) {
            observer.disconnect()
        }
        <span class="hljs-built_in">console</span>.log(entry)
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
<span class="hljs-comment">// The buffered property indicates whether to observe cached data, </span>
<span class="hljs-comment">// allowing observation even if the monitoring code is added after the event occurs</span>
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'paint'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>This code creates a new PerformanceObserver that watches for 'paint' type events. When the first-paint event occurs, it logs the entry information and disconnects the observer since we only need to capture this event once per page load. The observer's <code>observe()</code> method is configured with <code>buffered: true</code> to ensure we can catch paint events that occurred before our code runs.</p>
<p>The FP measurement output:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">entryType</span>: <span class="hljs-string">"paint"</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">"first-paint"</span>,
    <span class="hljs-attr">startTime</span>: <span class="hljs-number">359</span>, <span class="hljs-comment">// FP time</span>
}
</code></pre>
<p>The <code>startTime</code> value represents the paint timing we need. This value (359ms in this example) tells us how long it took from the start of navigation until the first visual change appeared on screen. You can use this metric to optimize your critical rendering path and reduce the time users spend looking at a blank screen.</p>
<h3 id="heading-fcp-first-contentful-paint">FCP (First Contentful Paint)</h3>
<p>FCP (First Contentful Paint) refers to the time from page load start until any part of page content is rendered. The "content" in this metric refers to text, images (including background images), <code>&lt;svg&gt;</code> elements, and non-white <code>&lt;canvas&gt;</code> elements.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/8a8b6762583fbb357b8ac6488a1f972e583bab8e6dfaab599e9a5ca3ea5e2403/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f61346631633962363130323934343864616532623163666235376234656637357e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/8a8b6762583fbb357b8ac6488a1f972e583bab8e6dfaab599e9a5ca3ea5e2403/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f61346631633962363130323934343864616532623163666235376234656637357e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="FCP visualization showing content being painted on screen" width="600" height="400" loading="lazy"></a></p>
<p>To provide a good user experience, the FCP score should be kept under 1.8 seconds.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/5a0734e52cbed48e8639fe185204c237fd658e5d60560b18578c848d74dac12c/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f39383138633636383739623334356533623438343566663366653031653863397e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/5a0734e52cbed48e8639fe185204c237fd658e5d60560b18578c848d74dac12c/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f39383138633636383739623334356533623438343566663366653031653863397e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="FCP scoring scale: Good (0-1.8s), Needs Improvement (1.8-3s), Poor (3s+)" width="600" height="400" loading="lazy"></a></p>
<p>The measurement code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {        
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-keyword">if</span> (entry.name === <span class="hljs-string">'first-contentful-paint'</span>) {
            observer.disconnect()
        }

        <span class="hljs-built_in">console</span>.log(entry)
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'paint'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>We can get the value of FCP via the above code:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">entryType</span>: <span class="hljs-string">"paint"</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">"first-contentful-paint"</span>,
    <span class="hljs-attr">startTime</span>: <span class="hljs-number">459</span>, <span class="hljs-comment">// fcp 时间</span>
}
</code></pre>
<p>The <code>startTime</code> value is the painting time we need.</p>
<h3 id="heading-lcp-largest-contentful-paint">LCP (Largest Contentful Paint)</h3>
<p>LCP (Largest Contentful Paint) refers to the time from page load start until the largest text block or image element completes rendering. The LCP metric reports the relative render time of the largest visible image or text block in the viewport, measured from when the page first begins loading.</p>
<p>A good LCP score should be kept under 2.5 seconds.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/76d0f2b9a24d36f12714e9ce39a61ce426eda6ae087c643745c71337352d5c27/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63303930646438623034326334366432616461626135333935636136386634377e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/76d0f2b9a24d36f12714e9ce39a61ce426eda6ae087c643745c71337352d5c27/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63303930646438623034326334366432616461626135333935636136386634377e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="LCP scoring scale: Good (0-2.5s), Needs Improvement (2.5-4s), Poor (4s+)" width="600" height="400" loading="lazy"></a></p>
<p>The measurement code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (observer) {
        observer.disconnect()
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-built_in">console</span>.log(entry)
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'largest-contentful-paint'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>We can get the value of LCP via the above code:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">element</span>: p,
    <span class="hljs-attr">entryType</span>: <span class="hljs-string">"largest-contentful-paint"</span>,
    <span class="hljs-attr">id</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">loadTime</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">renderTime</span>: <span class="hljs-number">1021.299</span>,
    <span class="hljs-attr">size</span>: <span class="hljs-number">37932</span>,
    <span class="hljs-attr">startTime</span>: <span class="hljs-number">1021.299</span>,
    <span class="hljs-attr">url</span>: <span class="hljs-string">""</span>,
}
</code></pre>
<p>The <code>startTime</code> value is the painting time we need. And <code>element</code> refers to the element being painted during LCP.</p>
<p>The difference between FCP and LCP is: FCP event occurs when any content is painted, while LCP event occurs when the largest content finishes rendering.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/ed1fac2eb0ad92326cb76a6f71f7c661164d881fe13f18e8de9a1d7f9423ad53/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653634363337616339643234336135383130316438656430316665383836657e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/ed1fac2eb0ad92326cb76a6f71f7c661164d881fe13f18e8de9a1d7f9423ad53/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653634363337616339643234336135383130316438656430316665383836657e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Comparison of FCP and LCP timing on webpage loading timeline" width="600" height="400" loading="lazy"></a></p>
<p>LCP considers these elements:</p>
<ul>
<li><p><code>&lt;img&gt;</code> elements</p>
</li>
<li><p><code>&lt;image&gt;</code> elements inside <code>&lt;svg&gt;</code></p>
</li>
<li><p><code>&lt;video&gt;</code> elements (using poster images)</p>
</li>
<li><p>Elements with background images loaded via the <a target="_blank" href="https://developer.mozilla.org/docs/Web/CSS/url\(\)"><code>url()</code></a> function (not using <a target="_blank" href="https://developer.mozilla.org/docs/Web/CSS/CSS_Images/Using_CSS_gradients">CSS gradients</a>)</p>
</li>
<li><p>Block-level elements containing text nodes or other inline-level text elements</p>
</li>
</ul>
<h3 id="heading-cls-cumulative-layout-shift">CLS (Cumulative Layout Shift)</h3>
<p>CLS (Cumulative Layout Shift) refers to the cumulative score of all unexpected layout shifts occurring between page load start and when the <a target="_blank" href="https://developer.chrome.com/docs/web-platform/page-lifecycle-api">page's lifecycle state</a> becomes hidden.</p>
<p>An "unexpected layout shift" occurs when elements on a page move around without user interaction. Common examples include:</p>
<ul>
<li><p>A banner or ad suddenly appearing at the top of the page, pushing content down</p>
</li>
<li><p>A font loading and changing the size of text</p>
</li>
<li><p>An image loading without predefined dimensions, expanding and pushing other content out of the way</p>
</li>
<li><p>A button appearing below where a user is about to click, causing them to click the wrong element</p>
</li>
</ul>
<p>These shifts are frustrating for users and lead to accidental clicks, lost reading position, and overall poor user experience. CLS helps quantify this problem so you can identify and fix problematic elements.</p>
<p>The layout shift score is calculated as follows:</p>
<pre><code class="lang-javascript">layout shift score = impact score × distance score
</code></pre>
<p>The <a target="_blank" href="https://github.com/WICG/layout-instability#Impact-Fraction">impact score</a> measures how <em>unstable elements</em> affect the visible area between two frames. The <em>distance score</em> is calculated by taking the greatest distance any <em>unstable element</em> has moved (either horizontally or vertically) and dividing it by the viewport's largest dimension (width or height, whichever is greater).</p>
<p><strong>CLS is the sum of all layout shift scores.</strong></p>
<p>A layout shift occurs when a DOM element changes position between two rendered frames, as shown below:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/d70250d691a70bb776e1b6748c2b39c7c8ebf17a5e0299bc0bebe064a4d44d91/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f66663037643431633632343234386131623636633537363166303438326632637e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/d70250d691a70bb776e1b6748c2b39c7c8ebf17a5e0299bc0bebe064a4d44d91/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f66663037643431633632343234386131623636633537363166303438326632637e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Layout shift visualization showing element position change" width="600" height="400" loading="lazy"></a></p>
<p><a target="_blank" href="https://camo.githubusercontent.com/826f631d4e71a8eb8f53738821f788c864988cff7fae350b77a43b3b3c22d331/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f64306435616238313030633934383961393931646430626538653139386166307e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/826f631d4e71a8eb8f53738821f788c864988cff7fae350b77a43b3b3c22d331/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f64306435616238313030633934383961393931646430626538653139386166307e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Rectangle movement illustration demonstrating layout shift from top-left to right" width="600" height="400" loading="lazy"></a></p>
<p>In the above diagram, the rectangle moves from the top-left to the right side, counting as one layout shift. In CLS terminology, there's a concept called "session window": one or more individual layout shifts occurring in rapid succession, with less than 1 second between each shift and a maximum window duration of 5 seconds.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/e2a49800b87d502b7a81f69ebc4259c27ec3c101faac3a65df30cd080ec29a85/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63366166326563353639363434303133393632363435383230656662313664337e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/e2a49800b87d502b7a81f69ebc4259c27ec3c101faac3a65df30cd080ec29a85/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63366166326563353639363434303133393632363435383230656662313664337e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Session window concept showing multiple layout shifts grouped within time constraints" width="600" height="400" loading="lazy"></a></p>
<p>For example, in the second session window shown above, there are four layout shifts. Each shift must occur less than 1 second after the previous one, and the time between the first and last shifts must not exceed 5 seconds to qualify as a session window. If these conditions aren't met, it's considered a new session window. This specification comes from extensive experimentation and research by the Chrome team, as detailed in <a target="_blank" href="https://web.dev/blog/evolving-cls">Evolving the CLS metric</a>.</p>
<p>CLS has three calculation methods:</p>
<ol>
<li><p>Cumulative</p>
</li>
<li><p>Average of all session windows</p>
</li>
<li><p>Maximum of all session windows</p>
</li>
</ol>
<h4 id="heading-cumulative"><strong>Cumulative</strong></h4>
<p>This method adds up all layout shift scores from page load start. However, this approach disadvantages long-lived pages - the longer a page is open, the higher the CLS score becomes.</p>
<h4 id="heading-average-of-all-session-windows"><strong>Average of All Session Windows</strong></h4>
<p>This method calculates based on session windows rather than individual layout shifts, taking the average of all session window scores. However, this approach has limitations.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/b4e2797c9a0b1de2c2f0c291374a53cfcb6b5d84c79c4d045ce2a78de1ed22fc/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f34326535323038643833663334396462383463663461323731393461353766327e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/b4e2797c9a0b1de2c2f0c291374a53cfcb6b5d84c79c4d045ce2a78de1ed22fc/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f34326535323038643833663334396462383463663461323731393461353766327e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Comparison of session windows with different CLS scores showing averaging limitations" width="600" height="400" loading="lazy"></a></p>
<p>As shown above, if the first session window has a high CLS score and the second has a low score, averaging them masks the actual page behavior. The average doesn't reflect that the page had more shifts early on and fewer later.</p>
<h4 id="heading-maximum-of-all-session-windows"><strong>Maximum of All Session Windows</strong></h4>
<p>This is currently the optimal calculation method, using the highest session window score to reflect the worst-case scenario for layout shifts. For more details, see <a target="_blank" href="https://web.dev/blog/evolving-cls">Evolving the CLS metric</a>.</p>
<p>Below is the implementation code for the third calculation method:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> sessionValue = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> sessionEntries = []
<span class="hljs-keyword">const</span> cls = {
    <span class="hljs-attr">subType</span>: <span class="hljs-string">'layout-shift'</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">'layout-shift'</span>,
    <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
    <span class="hljs-attr">pageURL</span>: getPageURL(),
    <span class="hljs-attr">value</span>: <span class="hljs-number">0</span>,
}

<span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-comment">// Only count layout shifts without recent user input.</span>
        <span class="hljs-keyword">if</span> (!entry.hadRecentInput) {
            <span class="hljs-keyword">const</span> firstSessionEntry = sessionEntries[<span class="hljs-number">0</span>]
            <span class="hljs-keyword">const</span> lastSessionEntry = sessionEntries[sessionEntries.length - <span class="hljs-number">1</span>]

            <span class="hljs-comment">// If the entry occurred less than 1 second after the previous entry and</span>
            <span class="hljs-comment">// less than 5 seconds after the first entry in the session, include the</span>
            <span class="hljs-comment">// entry in the current session. Otherwise, start a new session.</span>
            <span class="hljs-keyword">if</span> (
                sessionValue
                &amp;&amp; entry.startTime - lastSessionEntry.startTime &lt; <span class="hljs-number">1000</span>
                &amp;&amp; entry.startTime - firstSessionEntry.startTime &lt; <span class="hljs-number">5000</span>
            ) {
                sessionValue += entry.value
                sessionEntries.push(formatCLSEntry(entry))
            } <span class="hljs-keyword">else</span> {
                sessionValue = entry.value
                sessionEntries = [formatCLSEntry(entry)]
            }

            <span class="hljs-comment">// If the current session value is larger than the current CLS value,</span>
            <span class="hljs-comment">// update CLS and the entries contributing to it.</span>
            <span class="hljs-keyword">if</span> (sessionValue &gt; cls.value) {
                cls.value = sessionValue
                cls.entries = sessionEntries
                cls.startTime = performance.now()
                lazyReportCache(deepCopy(cls))
            }
        }
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'layout-shift'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>A single layout shift measurement contains the following data:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">entryType</span>: <span class="hljs-string">"layout-shift"</span>,
  <span class="hljs-attr">hadRecentInput</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">lastInputTime</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">name</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">sources</span>: (<span class="hljs-number">2</span>) [LayoutShiftAttribution, LayoutShiftAttribution],
  <span class="hljs-attr">startTime</span>: <span class="hljs-number">1176.199999999255</span>,
  <span class="hljs-attr">value</span>: <span class="hljs-number">0.000005752046026677329</span>,
}
</code></pre>
<p>The <code>value</code> field represents the layout shift score.</p>
<h3 id="heading-domcontentloaded-and-load-events"><strong>DOMContentLoaded and Load Events</strong></h3>
<p>The <code>DOMContentLoaded</code> event is triggered when the HTML is fully loaded and parsed, without waiting for CSS, images, and iframes to load.</p>
<p>The <code>load</code> event is triggered when the entire page and all dependent resources such as stylesheets and images have finished loading.</p>
<p>Although these performance metrics are older, they still provide valuable insights into page behavior. Monitoring them remains necessary.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { lazyReportCache } <span class="hljs-keyword">from</span> <span class="hljs-string">'../utils/report'</span>

[<span class="hljs-string">'load'</span>, <span class="hljs-string">'DOMContentLoaded'</span>].forEach(<span class="hljs-function"><span class="hljs-params">type</span> =&gt;</span> onEvent(type))

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onEvent</span>(<span class="hljs-params">type</span>) </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">callback</span>(<span class="hljs-params"></span>) </span>{
        lazyReportCache({
            <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
            <span class="hljs-attr">subType</span>: type.toLocaleLowerCase(),
            <span class="hljs-attr">startTime</span>: performance.now(),
        })

        <span class="hljs-built_in">window</span>.removeEventListener(type, callback, <span class="hljs-literal">true</span>)
    }

    <span class="hljs-built_in">window</span>.addEventListener(type, callback, <span class="hljs-literal">true</span>)
}
</code></pre>
<h3 id="heading-first-screen-rendering-time"><strong>First Screen Rendering Time</strong></h3>
<p>In most cases, the first screen rendering time can be obtained through the <code>load</code> event. However, there are exceptions, such as asynchronously loaded images and DOM elements.</p>
<pre><code class="lang-javascript">&lt;script&gt;
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">document</span>.body.innerHTML = <span class="hljs-string">`
            &lt;div&gt;
                &lt;!-- lots of code... --&gt;
            &lt;/div&gt;
        `</span>
    }, <span class="hljs-number">3000</span>)
&lt;/script&gt;
</code></pre>
<p>In such cases, we cannot obtain the first screen rendering time through the <code>load</code> event. Instead, we need to use <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver">MutationObserver</a> to get the first screen rendering time. MutationObserver triggers events when the properties of the DOM elements it's monitoring change.</p>
<p>The process of calculating first screen rendering time:</p>
<ol>
<li><p>Use MutationObserver to monitor the document object, triggering events whenever DOM element properties change.</p>
</li>
<li><p>Check if the DOM element is in the first screen. If it is, call <code>performance.now()</code> in the <code>requestAnimationFrame()</code> callback function to get the current time as its rendering time.</p>
</li>
<li><p>Compare the rendering time of the last DOM element with the loading time of all images in the first screen, and use the maximum value as the first screen rendering time.</p>
</li>
</ol>
<h4 id="heading-monitoring-dom"><strong>Monitoring DOM</strong></h4>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> next = <span class="hljs-built_in">window</span>.requestAnimationFrame ? requestAnimationFrame : <span class="hljs-built_in">setTimeout</span>
<span class="hljs-keyword">const</span> ignoreDOMList = [<span class="hljs-string">'STYLE'</span>, <span class="hljs-string">'SCRIPT'</span>, <span class="hljs-string">'LINK'</span>]

observer = <span class="hljs-keyword">new</span> MutationObserver(<span class="hljs-function"><span class="hljs-params">mutationList</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> entry = {
        <span class="hljs-attr">children</span>: [],
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> mutation <span class="hljs-keyword">of</span> mutationList) {
        <span class="hljs-keyword">if</span> (mutation.addedNodes.length &amp;&amp; isInScreen(mutation.target)) {
             <span class="hljs-comment">// ...</span>
        }
    }

    <span class="hljs-keyword">if</span> (entry.children.length) {
        entries.push(entry)
        next(<span class="hljs-function">() =&gt;</span> {
            entry.startTime = performance.now()
        })
    }
})

observer.observe(<span class="hljs-built_in">document</span>, {
    <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span>,
})
</code></pre>
<p>The code above monitors DOM changes while filtering out <code>style</code>, <code>script</code>, and <code>link</code> tags.</p>
<h4 id="heading-checking-if-element-is-in-first-screen"><strong>Checking if Element is in First Screen</strong></h4>
<p>A page may have a lot of content, but users can only see one screen at a time. Therefore, when calculating first screen rendering time, we need to limit the scope to content visible in the current screen.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> viewportWidth = <span class="hljs-built_in">window</span>.innerWidth
<span class="hljs-keyword">const</span> viewportHeight = <span class="hljs-built_in">window</span>.innerHeight

<span class="hljs-comment">// Check if DOM element is in screen</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isInScreen</span>(<span class="hljs-params">dom</span>) </span>{
    <span class="hljs-keyword">const</span> rectInfo = dom.getBoundingClientRect()
    <span class="hljs-keyword">if</span> (
        rectInfo.left &gt;= <span class="hljs-number">0</span> 
        &amp;&amp; rectInfo.left &lt; viewportWidth
        &amp;&amp; rectInfo.top &gt;= <span class="hljs-number">0</span>
        &amp;&amp; rectInfo.top &lt; viewportHeight
    ) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
}
</code></pre>
<h4 id="heading-using-requestanimationframe-to-get-dom-rendering-time"><strong>Using</strong> <code>requestAnimationFrame()</code> to Get DOM Rendering Time</h4>
<p>When DOM changes trigger the MutationObserver event, it only means the DOM content can be read, not that it has been painted to the screen.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/1c118ccd17b38cf8054796f978f864bcb055b3db7659b9c4b84acc349c978531/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f36373233306335653538666634633639396265373735383635366534353034667e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/1c118ccd17b38cf8054796f978f864bcb055b3db7659b9c4b84acc349c978531/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f36373233306335653538666634633639396265373735383635366534353034667e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Browser rendering pipeline showing DOM content loaded but not yet painted" width="600" height="400" loading="lazy"></a></p>
<p>As shown in the image above, when the MutationObserver event is triggered, we can read that <code>document.body</code> already has content, but the left side of the screen hasn't painted anything yet. Therefore, we need to call <code>requestAnimationFrame()</code> to get the current time as the DOM rendering time after the browser has successfully painted.</p>
<h4 id="heading-comparing-with-all-image-loading-times-in-first-screen"><strong>Comparing with All Image Loading Times in First Screen</strong></h4>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getRenderTime</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> startTime = <span class="hljs-number">0</span>
    entries.forEach(<span class="hljs-function"><span class="hljs-params">entry</span> =&gt;</span> {
        <span class="hljs-keyword">if</span> (entry.startTime &gt; startTime) {
            startTime = entry.startTime
        }
    })

    <span class="hljs-comment">// Need to compare with all image loading times in current page, take the maximum</span>
    <span class="hljs-comment">// Image request time must be less than startTime, response end time must be greater than startTime</span>
    performance.getEntriesByType(<span class="hljs-string">'resource'</span>).forEach(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> {
        <span class="hljs-keyword">if</span> (
            item.initiatorType === <span class="hljs-string">'img'</span>
            &amp;&amp; item.fetchStart &lt; startTime 
            &amp;&amp; item.responseEnd &gt; startTime
        ) {
            startTime = item.responseEnd
        }
    })

    <span class="hljs-keyword">return</span> startTime
}
</code></pre>
<h4 id="heading-optimization"><strong>Optimization</strong></h4>
<p>The current code still needs optimization, with two main points to consider:</p>
<ol>
<li><p>When should we report the rendering time?</p>
</li>
<li><p>How to handle asynchronously added DOM elements?</p>
</li>
</ol>
<p>For the first point, we must report the rendering time after DOM changes stop, which typically happens after the load event triggers. Therefore, we can report at this point.</p>
<p>For the second point, we can report after the LCP event triggers. Whether DOM elements are loaded synchronously or asynchronously, they need to be painted, so we can monitor the LCP event and only allow reporting after it triggers.</p>
<p>Combining these two approaches, we get the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> isOnLoaded = <span class="hljs-literal">false</span>
executeAfterLoad(<span class="hljs-function">() =&gt;</span> {
    isOnLoaded = <span class="hljs-literal">true</span>
})

<span class="hljs-keyword">let</span> timer
<span class="hljs-keyword">let</span> observer
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkDOMChange</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">clearTimeout</span>(timer)
    timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">// Calculate first screen rendering time after load and LCP events trigger and DOM tree stops changing</span>
        <span class="hljs-keyword">if</span> (isOnLoaded &amp;&amp; isLCPDone()) {
            observer &amp;&amp; observer.disconnect()
            lazyReportCache({
                <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
                <span class="hljs-attr">subType</span>: <span class="hljs-string">'first-screen-paint'</span>,
                <span class="hljs-attr">startTime</span>: getRenderTime(),
                <span class="hljs-attr">pageURL</span>: getPageURL(),
            })

            entries = <span class="hljs-literal">null</span>
        } <span class="hljs-keyword">else</span> {
            checkDOMChange()
        }
    }, <span class="hljs-number">500</span>)
}
</code></pre>
<p>The <code>checkDOMChange()</code> function is called each time the MutationObserver event triggers and needs to be debounced.</p>
<h3 id="heading-api-request-timing"><strong>API Request Timing</strong></h3>
<p>To monitor API request timing, we need to intercept both XMLHttpRequest and fetch requests.</p>
<p><strong>Monitoring XMLHttpRequest</strong></p>
<pre><code class="lang-javascript">originalProto.open = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">newOpen</span>(<span class="hljs-params">...args</span>) </span>{
    <span class="hljs-built_in">this</span>.url = args[<span class="hljs-number">1</span>]
    <span class="hljs-built_in">this</span>.method = args[<span class="hljs-number">0</span>]
    originalOpen.apply(<span class="hljs-built_in">this</span>, args)
}

originalProto.send = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">newSend</span>(<span class="hljs-params">...args</span>) </span>{
    <span class="hljs-built_in">this</span>.startTime = <span class="hljs-built_in">Date</span>.now()

    <span class="hljs-keyword">const</span> onLoadend = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">this</span>.endTime = <span class="hljs-built_in">Date</span>.now()
        <span class="hljs-built_in">this</span>.duration = <span class="hljs-built_in">this</span>.endTime - <span class="hljs-built_in">this</span>.startTime

        <span class="hljs-keyword">const</span> { status, duration, startTime, endTime, url, method } = <span class="hljs-built_in">this</span>
        <span class="hljs-keyword">const</span> reportData = {
            status,
            duration,
            startTime,
            endTime,
            url,
            <span class="hljs-attr">method</span>: (method || <span class="hljs-string">'GET'</span>).toUpperCase(),
            <span class="hljs-attr">success</span>: status &gt;= <span class="hljs-number">200</span> &amp;&amp; status &lt; <span class="hljs-number">300</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'xhr'</span>,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
        }

        lazyReportCache(reportData)

        <span class="hljs-built_in">this</span>.removeEventListener(<span class="hljs-string">'loadend'</span>, onLoadend, <span class="hljs-literal">true</span>)
    }

    <span class="hljs-built_in">this</span>.addEventListener(<span class="hljs-string">'loadend'</span>, onLoadend, <span class="hljs-literal">true</span>)
    originalSend.apply(<span class="hljs-built_in">this</span>, args)
}
</code></pre>
<p>To determine if an XML request is successful, we can check if its status code is between 200 and 299. If it is, the request was successful; otherwise, it failed.</p>
<p><strong>Monitoring fetch</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> originalFetch = <span class="hljs-built_in">window</span>.fetch

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">overwriteFetch</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">window</span>.fetch = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">newFetch</span>(<span class="hljs-params">url, config</span>) </span>{
        <span class="hljs-keyword">const</span> startTime = <span class="hljs-built_in">Date</span>.now()
        <span class="hljs-keyword">const</span> reportData = {
            startTime,
            url,
            <span class="hljs-attr">method</span>: (config?.method || <span class="hljs-string">'GET'</span>).toUpperCase(),
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'fetch'</span>,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
        }

        <span class="hljs-keyword">return</span> originalFetch(url, config)
        .then(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> {
            reportData.endTime = <span class="hljs-built_in">Date</span>.now()
            reportData.duration = reportData.endTime - reportData.startTime

            <span class="hljs-keyword">const</span> data = res.clone()
            reportData.status = data.status
            reportData.success = data.ok

            lazyReportCache(reportData)

            <span class="hljs-keyword">return</span> res
        })
        .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
            reportData.endTime = <span class="hljs-built_in">Date</span>.now()
            reportData.duration = reportData.endTime - reportData.startTime
            reportData.status = <span class="hljs-number">0</span>
            reportData.success = <span class="hljs-literal">false</span>

            lazyReportCache(reportData)

            <span class="hljs-keyword">throw</span> err
        })
    }
}
</code></pre>
<p>For fetch requests, we can determine success by checking the <code>ok</code> field in the response data. If it's <code>true</code>, the request was successful; otherwise, it failed.</p>
<p><strong>Note</strong>: The API request timing we monitor may differ from what's shown in Chrome DevTools. This is because Chrome DevTools shows the time for the entire HTTP request and interface process. But XHR and fetch are asynchronous requests – after the interface request succeeds, the callback function needs to be called. When the event triggers, the callback function is placed in the message queue, and then the browser processes it. There's also a waiting period in between.</p>
<h3 id="heading-resource-loading-time-and-cache-hit-rate"><strong>Resource Loading Time and Cache Hit Rate</strong></h3>
<p>We can monitor <code>resource</code> and <code>navigation</code> events through <code>PerformanceObserver</code>. If the browser doesn't support <code>PerformanceObserver</code>, we can fall back to using <code>performance.getEntriesByType(entryType)</code>.</p>
<p>When the <code>resource</code> event triggers, we can get the corresponding resource list. Each resource object contains the following fields:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/cf1c72dd2f2e45626e1f601a11b5c1a9a829125d30bc291f4f2bedc8a7d2391c/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653663623330616539613434343762626534336266636666366336633461317e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/cf1c72dd2f2e45626e1f601a11b5c1a9a829125d30bc291f4f2bedc8a7d2391c/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653663623330616539613434343762626534336266636666366336633461317e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Resource object fields in PerformanceResourceTiming interface" width="600" height="400" loading="lazy"></a></p>
<p>From these fields, we can extract useful information:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">name</span>: entry.name, <span class="hljs-comment">// Resource name</span>
    <span class="hljs-attr">subType</span>: entryType,
    <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
    <span class="hljs-attr">sourceType</span>: entry.initiatorType, <span class="hljs-comment">// Resource type</span>
    <span class="hljs-attr">duration</span>: entry.duration, <span class="hljs-comment">// Resource loading duration</span>
    <span class="hljs-attr">dns</span>: entry.domainLookupEnd - entry.domainLookupStart, <span class="hljs-comment">// DNS duration</span>
    <span class="hljs-attr">tcp</span>: entry.connectEnd - entry.connectStart, <span class="hljs-comment">// TCP connection duration</span>
    <span class="hljs-attr">redirect</span>: entry.redirectEnd - entry.redirectStart, <span class="hljs-comment">// Redirect duration</span>
    <span class="hljs-attr">ttfb</span>: entry.responseStart, <span class="hljs-comment">// Time to first byte</span>
    <span class="hljs-attr">protocol</span>: entry.nextHopProtocol, <span class="hljs-comment">// Request protocol</span>
    <span class="hljs-attr">responseBodySize</span>: entry.encodedBodySize, <span class="hljs-comment">// Response body size</span>
    <span class="hljs-attr">responseHeaderSize</span>: entry.transferSize - entry.encodedBodySize, <span class="hljs-comment">// Response header size</span>
    <span class="hljs-attr">resourceSize</span>: entry.decodedBodySize, <span class="hljs-comment">// Resource size after decompression</span>
    <span class="hljs-attr">isCache</span>: isCache(entry), <span class="hljs-comment">// Whether cache was hit</span>
    <span class="hljs-attr">startTime</span>: performance.now(),
}
</code></pre>
<h4 id="heading-determining-if-resource-hit-cache">Determining if Resource Hit Cache</h4>
<p>Among these resource objects, there's a <code>transferSize</code> field that represents the size of the resource being fetched, including response header fields and response data size. If this value is 0, it means the resource was read directly from cache (forced cache). If this value is not 0 but the <code>encodedBodySize</code> field is 0, it means it used negotiated cache (<code>encodedBodySize</code> represents the size of the response data body).</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isCache</span>(<span class="hljs-params">entry</span>) </span>{
    <span class="hljs-comment">// Read directly from cache or 304</span>
    <span class="hljs-keyword">return</span> entry.transferSize === <span class="hljs-number">0</span> || (entry.transferSize !== <span class="hljs-number">0</span> &amp;&amp; entry.encodedBodySize === <span class="hljs-number">0</span>)
}
</code></pre>
<p>If it doesn't meet the above conditions, it means the cache was not hit. Then we can calculate the cache hit rate by dividing <code>all cached data/total data</code>.</p>
<h3 id="heading-browser-backforward-cache-bfc"><strong>Browser Back/Forward Cache (BFC)</strong></h3>
<p>BFC is a memory cache that saves the entire page in memory. When users navigate back, they can see the entire page immediately without refreshing. According to the article <a target="_blank" href="https://web.dev/bfcache/">bfcache</a>, Firefox and Safari have always supported BFC, while Chrome only supports it in high-version mobile browsers. But when I tested it, only Safari supported it – my Firefox version might have been different.</p>
<p>Still, BFC also has drawbacks. When users navigate back and restore the page from BFC, the original page's code won't execute again. For this reason, browsers provide a <code>pageshow</code> event where we can put code that needs to be executed again.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'pageshow'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">event</span>) </span>{
  <span class="hljs-comment">// If this property is true, it means the page was restored from BFC</span>
  <span class="hljs-keyword">if</span> (event.persisted) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This page was restored from the bfcache.'</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This page was loaded normally.'</span>);
  }
});
</code></pre>
<p>For pages restored from BFC, we also need to collect their FP, FCP, LCP, and other timing metrics.</p>
<pre><code class="lang-javascript">onBFCacheRestore(<span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    requestAnimationFrame(<span class="hljs-function">() =&gt;</span> {
        [<span class="hljs-string">'first-paint'</span>, <span class="hljs-string">'first-contentful-paint'</span>].forEach(<span class="hljs-function"><span class="hljs-params">type</span> =&gt;</span> {
            lazyReportCache({
                <span class="hljs-attr">startTime</span>: performance.now() - event.timeStamp,
                <span class="hljs-attr">name</span>: type,
                <span class="hljs-attr">subType</span>: type,
                <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
                <span class="hljs-attr">pageURL</span>: getPageURL(),
                <span class="hljs-attr">bfc</span>: <span class="hljs-literal">true</span>,
            })
        })
    })
})
</code></pre>
<p>The code above is easy to understand. After the <code>pageshow</code> event triggers, we subtract the event timestamp from the current time – this time difference is the rendering time of the performance metrics.</p>
<p><strong>Note</strong>: For pages restored from BFC, these performance metrics usually have very small values, around 10 ms. This means that we need to add an identifier field <code>bfc: true</code> so we can ignore them when doing performance statistics.</p>
<h3 id="heading-fps"><strong>FPS</strong></h3>
<p>We can calculate the current page's FPS using <code>requestAnimationFrame()</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> next = <span class="hljs-built_in">window</span>.requestAnimationFrame 
    ? requestAnimationFrame : <span class="hljs-function">(<span class="hljs-params">callback</span>) =&gt;</span> { <span class="hljs-built_in">setTimeout</span>(callback, <span class="hljs-number">1000</span> / <span class="hljs-number">60</span>) }

<span class="hljs-keyword">const</span> frames = []

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fps</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> frame = <span class="hljs-number">0</span>
    <span class="hljs-keyword">let</span> lastSecond = <span class="hljs-built_in">Date</span>.now()

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateFPS</span>(<span class="hljs-params"></span>) </span>{
        frame++
        <span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now()
        <span class="hljs-keyword">if</span> (lastSecond + <span class="hljs-number">1000</span> &lt;= now) {
            <span class="hljs-comment">// Since now - lastSecond is in milliseconds, frame needs to be multiplied by 1000</span>
            <span class="hljs-keyword">const</span> fps = <span class="hljs-built_in">Math</span>.round((frame * <span class="hljs-number">1000</span>) / (now - lastSecond))
            frames.push(fps)

            frame = <span class="hljs-number">0</span>
            lastSecond = now
        }

        <span class="hljs-comment">// Avoid reporting too frequently, cache a certain amount before reporting</span>
        <span class="hljs-keyword">if</span> (frames.length &gt;= <span class="hljs-number">60</span>) {
            report(deepCopy({
                frames,
                <span class="hljs-attr">type</span>: <span class="hljs-string">'performace'</span>,
                <span class="hljs-attr">subType</span>: <span class="hljs-string">'fps'</span>,
            }))

            frames.length = <span class="hljs-number">0</span>
        }

        next(calculateFPS)
    }

    calculateFPS()
}
</code></pre>
<p>The code logic is as follows:</p>
<p>First record an initial time, then each time <code>requestAnimationFrame()</code> triggers, increment the frame count by 1. After one second passes, we can get the current frame rate by dividing <code>frame count/elapsed time</code>.</p>
<p>When three consecutive FPS values below 20 appear, we can determine that the page has become unresponsive. This technique is based on the observation that smooth animations require at least 20 FPS to appear fluid to users.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isBlocking</span>(<span class="hljs-params">fpsList, below = <span class="hljs-number">20</span>, last = <span class="hljs-number">3</span></span>) </span>{
    <span class="hljs-keyword">let</span> count = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; fpsList.length; i++) {
        <span class="hljs-keyword">if</span> (fpsList[i] &amp;&amp; fpsList[i] &lt; below) {
            count++
        } <span class="hljs-keyword">else</span> {
            count = <span class="hljs-number">0</span>
        }

        <span class="hljs-keyword">if</span> (count &gt;= last) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}
</code></pre>
<h3 id="heading-vue-router-change-rendering-time"><strong>Vue Router Change Rendering Time</strong></h3>
<p>We already know how to calculate first screen rendering time, but how do we calculate the page rendering time caused by route changes in SPA applications? This article uses Vue as an example to explain my approach.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onVueRouter</span>(<span class="hljs-params">Vue, router</span>) </span>{
    <span class="hljs-keyword">let</span> isFirst = <span class="hljs-literal">true</span>
    <span class="hljs-keyword">let</span> startTime
    router.beforeEach(<span class="hljs-function">(<span class="hljs-params">to, <span class="hljs-keyword">from</span>, next</span>) =&gt;</span> {
        <span class="hljs-comment">// First page load already has other rendering time metrics available</span>
        <span class="hljs-keyword">if</span> (isFirst) {
            isFirst = <span class="hljs-literal">false</span>
            <span class="hljs-keyword">return</span> next()
        }

        <span class="hljs-comment">// Add a new field to router to indicate whether to calculate rendering time</span>
        <span class="hljs-comment">// Only needed for route changes</span>
        router.needCalculateRenderTime = <span class="hljs-literal">true</span>
        startTime = performance.now()

        next()
    })

    <span class="hljs-keyword">let</span> timer
    Vue.mixin({
        mounted() {
            <span class="hljs-keyword">if</span> (!router.needCalculateRenderTime) <span class="hljs-keyword">return</span>

            <span class="hljs-built_in">this</span>.$nextTick(<span class="hljs-function">() =&gt;</span> {
                <span class="hljs-comment">// Code that only runs after the entire view has been rendered</span>
                <span class="hljs-keyword">const</span> now = performance.now()
                <span class="hljs-built_in">clearTimeout</span>(timer)

                timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
                    router.needCalculateRenderTime = <span class="hljs-literal">false</span>
                    lazyReportCache({
                        <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
                        <span class="hljs-attr">subType</span>: <span class="hljs-string">'vue-router-change-paint'</span>,
                        <span class="hljs-attr">duration</span>: now - startTime,
                        <span class="hljs-attr">startTime</span>: now,
                        <span class="hljs-attr">pageURL</span>: getPageURL(),
                    })
                }, <span class="hljs-number">1000</span>)
            })
        },
    })
}
</code></pre>
<p>The code logic is as follows:</p>
<ol>
<li><p>Monitor route hooks – when route changes occur, the <code>router.beforeEach()</code> hook triggers. In this hook's callback function, record the current time as the rendering start time.</p>
</li>
<li><p>Use <code>Vue.mixin()</code> to inject a function into all components' <code>mounted()</code> hooks. Each function executes a debounced function.</p>
</li>
<li><p>When the last component's <code>mounted()</code> triggers, it means all components under this route have been mounted. We can get the rendering time in the <code>this.$nextTick()</code> callback function.</p>
</li>
</ol>
<p>Also, we need to consider another case. When not changing routes, there may also be component changes, in which case we shouldn't calculate rendering time in these components' <code>mounted()</code> hooks. Therefore, we need to add a <code>needCalculateRenderTime</code> field - set it to true when changing routes to indicate that rendering time can be calculated.</p>
<h2 id="heading-error-data-collection"><strong>Error Data Collection</strong></h2>
<p>Error monitoring is a critical aspect of frontend monitoring that helps identify issues users encounter while interacting with your application. By tracking and analyzing these errors, you can proactively fix bugs before they affect more users, improving both user experience and application reliability.</p>
<p>In this section, we'll explore how to capture various types of errors including resource loading failures, JavaScript runtime errors, unhandled promises, and framework-specific errors.</p>
<h3 id="heading-resource-loading-errors"><strong>Resource Loading Errors</strong></h3>
<p>Resource loading errors occur when the browser fails to load external resources like images, stylesheets, scripts, and fonts. These errors can significantly impact user experience by causing missing content, broken layouts, or even preventing core functionality from working.</p>
<p>Using <code>addEventListener()</code> to monitor the error event can capture resource loading failure errors.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Capture resource loading failure errors js css img...</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'error'</span>, <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> target = e.target
    <span class="hljs-keyword">if</span> (!target) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">if</span> (target.src || target.href) {
        <span class="hljs-keyword">const</span> url = target.src || target.href
        lazyReportCache({
            url,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'resource'</span>,
            <span class="hljs-attr">startTime</span>: e.timeStamp,
            <span class="hljs-attr">html</span>: target.outerHTML,
            <span class="hljs-attr">resourceType</span>: target.tagName,
            <span class="hljs-attr">paths</span>: e.path.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.tagName).filter(<span class="hljs-built_in">Boolean</span>),
            <span class="hljs-attr">pageURL</span>: getPageURL(),
        })
    }
}, <span class="hljs-literal">true</span>)
</code></pre>
<p>This code listens for the global <code>error</code> event with the capture option set to true, which allows it to catch errors from resource elements like <code>&lt;img&gt;</code>, <code>&lt;link&gt;</code>, and <code>&lt;script&gt;</code>. When a resource fails to load, it collects important information including:</p>
<ul>
<li><p>The URL of the failed resource</p>
</li>
<li><p>The element type (img, link, script)</p>
</li>
<li><p>The HTML of the element that failed</p>
</li>
<li><p>The DOM path to the element</p>
</li>
<li><p>The page URL where the error occurred</p>
</li>
</ul>
<p>With this data, you can identify which resources are failing most frequently, prioritize fixes, and implement fallback strategies for critical resources.</p>
<h3 id="heading-javascript-errors"><strong>JavaScript Errors</strong></h3>
<p>JavaScript errors occur during script execution and can prevent features from working properly. These include syntax errors, reference errors, type errors, and other runtime exceptions.</p>
<p>Using <code>window.onerror</code> can monitor JavaScript errors.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Monitor JavaScript errors</span>
<span class="hljs-built_in">window</span>.onerror = <span class="hljs-function">(<span class="hljs-params">msg, url, line, column, error</span>) =&gt;</span> {
    lazyReportCache({
        msg,
        line,
        column,
        <span class="hljs-attr">error</span>: error.stack,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'js'</span>,
        <span class="hljs-attr">pageURL</span>: url,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
        <span class="hljs-attr">startTime</span>: performance.now(),
    })
}
</code></pre>
<p>This handler captures detailed information about JavaScript errors:</p>
<ul>
<li><p>The error message</p>
</li>
<li><p>The file URL where the error occurred</p>
</li>
<li><p>The line and column number of the error</p>
</li>
<li><p>The full error stack trace</p>
</li>
</ul>
<p>This information is invaluable for debugging and fixing issues, especially in production environments where direct debugging isn't possible. By analyzing these errors, you can identify patterns and prioritize fixes for the most common or impactful issues.</p>
<h3 id="heading-promise-errors"><strong>Promise Errors</strong></h3>
<p>Modern JavaScript applications heavily use Promises for asynchronous operations. When a Promise rejection isn't handled with <code>.catch()</code> or a second argument to <code>.then()</code>, it results in an unhandled rejection which can cause silent failures.</p>
<p>Using <code>addEventListener()</code> to monitor the unhandledrejection event can capture unhandled promise errors.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Monitor promise errors - drawback is can't get column data</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'unhandledrejection'</span>, <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    lazyReportCache({
        <span class="hljs-attr">reason</span>: e.reason?.stack,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'promise'</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
        <span class="hljs-attr">startTime</span>: e.timeStamp,
        <span class="hljs-attr">pageURL</span>: getPageURL(),
    })
})
</code></pre>
<p>This code captures unhandled Promise rejections and reports:</p>
<ul>
<li><p>The rejection reason (usually an error object with a stack trace)</p>
</li>
<li><p>The timestamp when the rejection occurred</p>
</li>
<li><p>The page URL where the rejection happened</p>
</li>
</ul>
<p>Tracking unhandled Promise rejections is particularly important for asynchronous operations like API calls, where errors might otherwise go unnoticed. By monitoring these rejections, you can ensure that all asynchronous errors are properly handled and resolved.</p>
<h3 id="heading-sourcemap"><strong>Sourcemap</strong></h3>
<p>Generally, production environment code is minified, and sourcemap files are not uploaded to production. Therefore, error messages in production environment code are difficult to read. For this reason, we can use <a target="_blank" href="https://github.com/mozilla/source-map">source-map</a> to restore these minified code error messages.</p>
<p>When code errors occur, we can get the corresponding filename, line number, and column number:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">line</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">column</span>: <span class="hljs-number">17</span>,
    <span class="hljs-attr">file</span>: <span class="hljs-string">'https:/www.xxx.com/bundlejs'</span>,
}
</code></pre>
<p>Then call the following code to restore:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parse</span>(<span class="hljs-params">error</span>) </span>{
    <span class="hljs-keyword">const</span> mapObj = <span class="hljs-built_in">JSON</span>.parse(getMapFileContent(error.url))
    <span class="hljs-keyword">const</span> consumer = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> sourceMap.SourceMapConsumer(mapObj)
    <span class="hljs-comment">// Remove ./ from webpack://source-map-demo/./src/index.js file</span>
    <span class="hljs-keyword">const</span> sources = mapObj.sources.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> format(item))
    <span class="hljs-comment">// Get original line and column numbers and source file based on minified error information</span>
    <span class="hljs-keyword">const</span> originalInfo = consumer.originalPositionFor({ <span class="hljs-attr">line</span>: error.line, <span class="hljs-attr">column</span>: error.column })
    <span class="hljs-comment">// sourcesContent contains the original source code of each file before minification, find corresponding source code by filename</span>
    <span class="hljs-keyword">const</span> originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">file</span>: originalInfo.source,
        <span class="hljs-attr">content</span>: originalFileContent,
        <span class="hljs-attr">line</span>: originalInfo.line,
        <span class="hljs-attr">column</span>: originalInfo.column,
        <span class="hljs-attr">msg</span>: error.msg,
        <span class="hljs-attr">error</span>: error.error
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">format</span>(<span class="hljs-params">item</span>) </span>{
    <span class="hljs-keyword">return</span> item.replace(<span class="hljs-regexp">/(\.\/)*/g</span>, <span class="hljs-string">''</span>)
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMapFileContent</span>(<span class="hljs-params">url</span>) </span>{
    <span class="hljs-keyword">return</span> fs.readFileSync(path.resolve(__dirname, <span class="hljs-string">`./maps/<span class="hljs-subst">${url.split(<span class="hljs-string">'/'</span>).pop()}</span>.map`</span>), <span class="hljs-string">'utf-8'</span>)
}
</code></pre>
<p>Each time the project is built, if sourcemap is enabled, each JS file will have a corresponding map file.</p>
<pre><code class="lang-javascript">bundle.js
bundle.js.map
</code></pre>
<p>At this point, the JS file is placed on the static server for user access, while the map file is stored on the server for error message restoration. The <code>source-map</code> library can restore error messages from minified code to their original state. For example, if the minified code error location is <code>line 1, column 47</code>, the restored location might be <code>line 4, column 10</code>. Besides location information, we can also get the original source code.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/be3d758960a8e8e494066820ef5708d8d06bda3df1a380bab37e007a91d16003/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f62316336623565656262376234656635396434646436616436313334383465627e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/be3d758960a8e8e494066820ef5708d8d06bda3df1a380bab37e007a91d16003/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f62316336623565656262376234656635396434646436616436313334383465627e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Sourcemap error restoration example showing minified vs original code" width="600" height="400" loading="lazy"></a></p>
<p>The image above shows an example of code error restoration. Since this part doesn't belong to the SDK's scope, I created another <a target="_blank" href="https://github.com/woai3c/source-map-demo">repository</a> to handle this. Feel free to check it out if you're interested.</p>
<h3 id="heading-vue-errors"><strong>Vue Errors</strong></h3>
<p>Using <code>window.onerror</code> cannot capture Vue errors – we need to use Vue's provided API for monitoring.</p>
<pre><code class="lang-javascript">Vue.config.errorHandler = <span class="hljs-function">(<span class="hljs-params">err, vm, info</span>) =&gt;</span> {
    <span class="hljs-comment">// Print error information to console</span>
    <span class="hljs-built_in">console</span>.error(err)

    lazyReportCache({
        info,
        <span class="hljs-attr">error</span>: err.stack,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'vue'</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
        <span class="hljs-attr">startTime</span>: performance.now(),
        <span class="hljs-attr">pageURL</span>: getPageURL(),
    })
}
</code></pre>
<h2 id="heading-behavior-data-collection"><strong>Behavior Data Collection</strong></h2>
<p>Understanding how users interact with your application is crucial for optimizing user experience, improving engagement, and driving business goals. Behavior monitoring tracks user actions, navigation patterns, and engagement metrics to provide insights into how your application is actually being used.</p>
<p>In this section, we'll explore how to collect key behavioral metrics that can help you make data-driven decisions to improve your application.</p>
<h3 id="heading-pv-and-uv"><strong>PV and UV</strong></h3>
<p>PV (Page View) is the number of page views, while UV (Unique Visitor) is the number of unique users visiting. PV counts each page visit, while UV only counts once per user per day.</p>
<p><strong>Why this matters</strong>: PV and UV metrics help you understand your application's traffic patterns. A high PV-to-UV ratio indicates users are viewing multiple pages, suggesting good engagement. Tracking these metrics over time helps you identify growth trends, seasonal patterns, and the impact of marketing campaigns or feature releases.</p>
<p>For the front end, we just need to report PV each time a page is entered. UV statistics are handled on the server side, mainly analyzing reported data to calculate UV.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pv</span>(<span class="hljs-params"></span>) </span>{
    lazyReportCache({
        <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'pv'</span>,
        <span class="hljs-attr">startTime</span>: performance.now(),
        <span class="hljs-attr">pageURL</span>: getPageURL(),
        <span class="hljs-attr">referrer</span>: <span class="hljs-built_in">document</span>.referrer,
        <span class="hljs-attr">uuid</span>: getUUID(),
    })
}
</code></pre>
<p>You can use this data to:</p>
<ul>
<li><p>Track which pages are most popular</p>
</li>
<li><p>Identify underperforming pages that need improvement</p>
</li>
<li><p>Analyze user flow through your application</p>
</li>
<li><p>Measure the effectiveness of new features or content</p>
</li>
</ul>
<h3 id="heading-page-stay-duration"><strong>Page Stay Duration</strong></h3>
<p>To get the stay duration, you just need to record an initial time when users enter the page, then subtract the initial time from the current time when users leave the page. This calculation logic can be placed in the <code>beforeunload</code> event.</p>
<p><strong>Why this matters</strong>: Page stay duration indicates how engaging your content is. Longer durations typically suggest users find the content valuable, while very short durations might indicate confusion, irrelevant content, or usability issues. This metric helps you identify which pages effectively capture user attention and which ones need improvement.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pageAccessDuration</span>(<span class="hljs-params"></span>) </span>{
    onBeforeunload(<span class="hljs-function">() =&gt;</span> {
        report({
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'page-access-duration'</span>,
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">pageURL</span>: getPageURL(),
            <span class="hljs-attr">uuid</span>: getUUID(),
        }, <span class="hljs-literal">true</span>)
    })
}
</code></pre>
<p>With page stay duration data, you can:</p>
<ul>
<li><p>Identify engaging vs. problematic content</p>
</li>
<li><p>Set benchmarks for content performance</p>
</li>
<li><p>Detect potential usability issues (extremely short durations)</p>
</li>
<li><p>Measure the effectiveness of content updates or redesigns</p>
</li>
</ul>
<h3 id="heading-page-access-depth"><strong>Page Access Depth</strong></h3>
<p>Recording page access depth is very useful. For example, for different activity pages a and b, if page a has an average access depth of 50% and page b has 80%, it indicates that page b is more popular with users. Based on this, we can make targeted improvements to page a.</p>
<p><strong>Why this matters</strong>: Access depth measures how far users scroll down a page, revealing whether they're viewing all your content or abandoning it partway through. This metric helps identify content engagement patterns and potential issues with content structure or page length.</p>
<p>Also, we can use access depth and stay duration to identify e-commerce order fraud. For example, if someone enters the page and immediately scrolls to the bottom, then waits a while before purchasing, while another person slowly scrolls down the page before purchasing. Even though they have the same stay duration, the first person is more likely to be committing fraud.</p>
<p>The page access depth calculation process is slightly more complex:</p>
<ol>
<li><p>When users enter the page, record the current time, scrollTop value, viewport height, and total page height.</p>
</li>
<li><p>When users scroll the page, the <code>scroll</code> event triggers. In the callback function, use the data from point 1 to calculate page access depth and stay duration.</p>
</li>
<li><p>When users stop scrolling at a certain point to continue viewing the page, record the current time, scrollTop value, viewport height, and total page height.</p>
</li>
<li><p>Repeat point 2...</p>
</li>
</ol>
<p>Here's the specific code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> timer
<span class="hljs-keyword">let</span> startTime = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> hasReport = <span class="hljs-literal">false</span>
<span class="hljs-keyword">let</span> pageHeight = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> scrollTop = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> viewportHeight = <span class="hljs-number">0</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pageAccessHeight</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'scroll'</span>, onScroll)

    onBeforeunload(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> now = performance.now()
        report({
            <span class="hljs-attr">startTime</span>: now,
            <span class="hljs-attr">duration</span>: now - startTime,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'page-access-height'</span>,
            <span class="hljs-attr">pageURL</span>: getPageURL(),
            <span class="hljs-attr">value</span>: toPercent((scrollTop + viewportHeight) / pageHeight),
            <span class="hljs-attr">uuid</span>: getUUID(),
        }, <span class="hljs-literal">true</span>)
    })

    <span class="hljs-comment">// Initialize and record current access height and time after page loads</span>
    executeAfterLoad(<span class="hljs-function">() =&gt;</span> {
        startTime = performance.now()
        pageHeight = <span class="hljs-built_in">document</span>.documentElement.scrollHeight || <span class="hljs-built_in">document</span>.body.scrollHeight
        scrollTop = <span class="hljs-built_in">document</span>.documentElement.scrollTop || <span class="hljs-built_in">document</span>.body.scrollTop
        viewportHeight = <span class="hljs-built_in">window</span>.innerHeight
    })
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onScroll</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">clearTimeout</span>(timer)
    <span class="hljs-keyword">const</span> now = performance.now()

    <span class="hljs-keyword">if</span> (!hasReport) {
        hasReport = <span class="hljs-literal">true</span>
        lazyReportCache({
            <span class="hljs-attr">startTime</span>: now,
            <span class="hljs-attr">duration</span>: now - startTime,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'page-access-height'</span>,
            <span class="hljs-attr">pageURL</span>: getPageURL(),
            <span class="hljs-attr">value</span>: toPercent((scrollTop + viewportHeight) / pageHeight),
            <span class="hljs-attr">uuid</span>: getUUID(),
        })
    }

    timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        hasReport = <span class="hljs-literal">false</span>
        startTime = now
        pageHeight = <span class="hljs-built_in">document</span>.documentElement.scrollHeight || <span class="hljs-built_in">document</span>.body.scrollHeight
        scrollTop = <span class="hljs-built_in">document</span>.documentElement.scrollTop || <span class="hljs-built_in">document</span>.body.scrollTop
        viewportHeight = <span class="hljs-built_in">window</span>.innerHeight        
    }, <span class="hljs-number">500</span>)
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toPercent</span>(<span class="hljs-params">val</span>) </span>{
    <span class="hljs-keyword">if</span> (val &gt;= <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> <span class="hljs-string">'100%'</span>
    <span class="hljs-keyword">return</span> (val * <span class="hljs-number">100</span>).toFixed(<span class="hljs-number">2</span>) + <span class="hljs-string">'%'</span>
}
</code></pre>
<p>With page access depth data, you can:</p>
<ul>
<li><p>Identify where users lose interest in your content</p>
</li>
<li><p>Optimize content placement (put important elements where users actually look)</p>
</li>
<li><p>Improve long-form content structure with better hierarchy</p>
</li>
<li><p>Detect unusual user behavior patterns that might indicate fraud or bots</p>
</li>
</ul>
<h3 id="heading-user-clicks"><strong>User Clicks</strong></h3>
<p>Using <code>addEventListener()</code> to monitor <code>mousedown</code> and <code>touchstart</code> events, we can collect information about each click area's size, click coordinates' specific position in the page, clicked element's content, and other information.</p>
<p><strong>Why this matters</strong>: Click tracking reveals what elements users interact with most frequently, helping you understand user interests and optimize UI element placement. It also helps identify usability issues where users might be clicking on non-interactive elements expecting a response.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onClick</span>(<span class="hljs-params"></span>) </span>{
    [<span class="hljs-string">'mousedown'</span>, <span class="hljs-string">'touchstart'</span>].forEach(<span class="hljs-function"><span class="hljs-params">eventType</span> =&gt;</span> {
        <span class="hljs-keyword">let</span> timer
        <span class="hljs-built_in">window</span>.addEventListener(eventType, <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
            <span class="hljs-built_in">clearTimeout</span>(timer)
            timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
                <span class="hljs-keyword">const</span> target = event.target
                <span class="hljs-keyword">const</span> { top, left } = target.getBoundingClientRect()

                lazyReportCache({
                    top,
                    left,
                    eventType,
                    <span class="hljs-attr">pageHeight</span>: <span class="hljs-built_in">document</span>.documentElement.scrollHeight || <span class="hljs-built_in">document</span>.body.scrollHeight,
                    <span class="hljs-attr">scrollTop</span>: <span class="hljs-built_in">document</span>.documentElement.scrollTop || <span class="hljs-built_in">document</span>.body.scrollTop,
                    <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
                    <span class="hljs-attr">subType</span>: <span class="hljs-string">'click'</span>,
                    <span class="hljs-attr">target</span>: target.tagName,
                    <span class="hljs-attr">paths</span>: event.path?.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.tagName).filter(<span class="hljs-built_in">Boolean</span>),
                    <span class="hljs-attr">startTime</span>: event.timeStamp,
                    <span class="hljs-attr">pageURL</span>: getPageURL(),
                    <span class="hljs-attr">outerHTML</span>: target.outerHTML,
                    <span class="hljs-attr">innerHTML</span>: target.innerHTML,
                    <span class="hljs-attr">width</span>: target.offsetWidth,
                    <span class="hljs-attr">height</span>: target.offsetHeight,
                    <span class="hljs-attr">viewport</span>: {
                        <span class="hljs-attr">width</span>: <span class="hljs-built_in">window</span>.innerWidth,
                        <span class="hljs-attr">height</span>: <span class="hljs-built_in">window</span>.innerHeight,
                    },
                    <span class="hljs-attr">uuid</span>: getUUID(),
                })
            }, <span class="hljs-number">500</span>)
        })
    })
}
</code></pre>
<p>With this click data, you can:</p>
<ul>
<li><p>Create heatmaps showing where users click most frequently</p>
</li>
<li><p>Identify non-functional elements users try to interact with</p>
</li>
<li><p>Optimize button placement and size for better conversion</p>
</li>
<li><p>Detect rage clicks (multiple rapid clicks in the same area) indicating user frustration</p>
</li>
</ul>
<h3 id="heading-page-navigation"><strong>Page Navigation</strong></h3>
<p>Using <code>addEventListener()</code> to monitor <code>popstate</code> and <code>hashchange</code> page navigation events allows you to track how users navigate through your application.</p>
<p><strong>Why this matters</strong>: Navigation tracking helps you understand user flow patterns - how users move between pages, which navigation paths are most common, and where users might be getting lost or trapped in navigation loops. This data is crucial for optimizing site structure and improving user journey flows.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pageChange</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">from</span> = <span class="hljs-string">''</span>
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'popstate'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> to = getPageURL()

        lazyReportCache({
            <span class="hljs-keyword">from</span>,
            to,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'popstate'</span>,
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">uuid</span>: getUUID(),
        })

        <span class="hljs-keyword">from</span> = to
    }, <span class="hljs-literal">true</span>)

    <span class="hljs-keyword">let</span> oldURL = <span class="hljs-string">''</span>
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'hashchange'</span>, <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
        <span class="hljs-keyword">const</span> newURL = event.newURL

        lazyReportCache({
            <span class="hljs-attr">from</span>: oldURL,
            <span class="hljs-attr">to</span>: newURL,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'hashchange'</span>,
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">uuid</span>: getUUID(),
        })

        oldURL = newURL
    }, <span class="hljs-literal">true</span>)
}
</code></pre>
<p>With navigation data, you can:</p>
<ul>
<li><p>Identify common user paths through your application</p>
</li>
<li><p>Detect navigation dead-ends or loops where users get stuck</p>
</li>
<li><p>Optimize navigation menus based on actual usage patterns</p>
</li>
<li><p>Improve information architecture to better match user behavior</p>
</li>
</ul>
<h3 id="heading-vue-router-changes"><strong>Vue Router Changes</strong></h3>
<p>For applications built with Vue, you can use the router's hooks to monitor navigation between routes, providing similar insights to general page navigation tracking but specific to Vue's routing system.</p>
<p><strong>Why this matters</strong>: In single-page applications, traditional page navigation events don't capture all route changes. Framework-specific router monitoring ensures you don't miss important navigation data in modern web applications.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onVueRouter</span>(<span class="hljs-params">router</span>) </span>{
    router.beforeEach(<span class="hljs-function">(<span class="hljs-params">to, <span class="hljs-keyword">from</span>, next</span>) =&gt;</span> {
        <span class="hljs-comment">// Don't count first page load</span>
        <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">from</span>.name) {
            <span class="hljs-keyword">return</span> next()
        }

        <span class="hljs-keyword">const</span> data = {
            <span class="hljs-attr">params</span>: to.params,
            <span class="hljs-attr">query</span>: to.query,
        }

        lazyReportCache({
            data,
            <span class="hljs-attr">name</span>: to.name || to.path,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: [<span class="hljs-string">'vue-router-change'</span>, <span class="hljs-string">'pv'</span>],
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">from</span>: <span class="hljs-keyword">from</span>.fullPath,
            <span class="hljs-attr">to</span>: to.fullPath,
            <span class="hljs-attr">uuid</span>: getUUID(),
        })

        next()
    })
}
</code></pre>
<p>This data helps you:</p>
<ul>
<li><p>Track the most frequently accessed routes in your Vue application</p>
</li>
<li><p>Understand navigation patterns specific to your application's structure</p>
</li>
<li><p>Identify potential optimization opportunities in your routing setup</p>
</li>
<li><p>Measure the impact of UX improvements on navigation behavior</p>
</li>
</ul>
<h2 id="heading-data-reporting"><strong>Data Reporting</strong></h2>
<p>Once you've collected performance, error, and behavior data, you need a reliable system to transmit this information to your backend for processing and analysis. Data reporting is the critical bridge between client-side data collection and server-side analytics.</p>
<p>Effective data reporting must balance several concerns:</p>
<ol>
<li><p><strong>Reliability</strong> – Ensuring data is successfully transmitted, especially critical errors</p>
</li>
<li><p><strong>Performance</strong> – Minimizing impact on the user experience and application performance</p>
</li>
<li><p><strong>Timing</strong> – Deciding when to send data to avoid interference with user interactions</p>
</li>
<li><p><strong>Bandwidth</strong> – Managing the amount of data transmitted to reduce network usage</p>
</li>
</ol>
<p>Let's explore the various methods and strategies for implementing efficient data reporting.</p>
<h3 id="heading-reporting-methods"><strong>Reporting Methods</strong></h3>
<p>Data can be reported using the following methods:</p>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></p>
</li>
<li><p>image</p>
</li>
</ul>
<p>My simple SDK uses a combination of the first and second methods for reporting. Using sendBeacon for reporting has very obvious advantages.</p>
<p><strong>Note</strong>: Using the <code>sendBeacon()</code> method will send data to the server asynchronously when the user agent has an opportunity, without delaying page unload or affecting the performance of the next navigation. This solves all the problems with submitting analytics data: data is reliable, transmission is asynchronous, and it won't affect the loading of the next page.</p>
<p>For browsers that don't support sendBeacon, we can use XMLHttpRequest for reporting. An HTTP request consists of sending and receiving two steps.</p>
<p>Actually, for reporting, we just need to ensure the data can be sent – we don't need to receive the response. For this reason, I did an experiment where I sent 30kb of data (generally reported data rarely exceeds this size) using XMLHttpRequest in beforeunload, tested with different browsers, and all were able to send successfully. Of course, this also depends on hardware performance and network conditions.</p>
<p>Here's a sample implementation of a reporting function that uses both methods:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">report</span>(<span class="hljs-params">data, isImmediate = false</span>) </span>{
    <span class="hljs-keyword">if</span> (!config.reportUrl) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Report URL is not set'</span>)
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-comment">// Add timestamp and other common properties</span>
    <span class="hljs-keyword">const</span> reportData = {
        ...data,
        <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
        <span class="hljs-attr">userAgent</span>: navigator.userAgent,
        <span class="hljs-attr">userId</span>: getUserId(),
        <span class="hljs-comment">// Add other common properties as needed</span>
    }

    <span class="hljs-comment">// Choose reporting method based on browser support and timing</span>
    <span class="hljs-keyword">if</span> (isImmediate) {
        sendData(reportData)
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Queue data for batch sending</span>
        reportQueue.push(reportData)

        <span class="hljs-comment">// Send when queue reaches threshold</span>
        <span class="hljs-keyword">if</span> (reportQueue.length &gt;= config.batchSize) {
            sendBatchData()
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendData</span>(<span class="hljs-params">data</span>) </span>{
    <span class="hljs-comment">// Try sendBeacon first</span>
    <span class="hljs-keyword">if</span> (navigator.sendBeacon) {
        <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">new</span> Blob([<span class="hljs-built_in">JSON</span>.stringify(data)], { <span class="hljs-attr">type</span>: <span class="hljs-string">'application/json'</span> })
        <span class="hljs-keyword">const</span> success = navigator.sendBeacon(config.reportUrl, blob)

        <span class="hljs-keyword">if</span> (success) <span class="hljs-keyword">return</span>
    }

    <span class="hljs-comment">// Fall back to XMLHttpRequest</span>
    <span class="hljs-keyword">const</span> xhr = <span class="hljs-keyword">new</span> XMLHttpRequest()
    xhr.open(<span class="hljs-string">'POST'</span>, config.reportUrl, <span class="hljs-literal">true</span>)
    xhr.setRequestHeader(<span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'application/json'</span>)
    xhr.send(<span class="hljs-built_in">JSON</span>.stringify(data))
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendBatchData</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (reportQueue.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">const</span> data = [...reportQueue]
    reportQueue.length = <span class="hljs-number">0</span>

    sendData({ <span class="hljs-attr">type</span>: <span class="hljs-string">'batch'</span>, data })
}
</code></pre>
<h3 id="heading-reporting-timing"><strong>Reporting Timing</strong></h3>
<p>There are three reporting timings:</p>
<ol>
<li><p>Use <code>requestIdleCallback/setTimeout</code> for delayed reporting</p>
</li>
<li><p>Report in the beforeunload callback function</p>
</li>
<li><p>Cache reported data and report when reaching a certain amount</p>
</li>
</ol>
<p>It's recommended to combine all three methods:</p>
<ol>
<li><p>First cache the reported data, and when reaching a certain amount, use <code>requestIdleCallback/setTimeout</code> for delayed reporting</p>
</li>
<li><p>Report all unreported data when leaving the page</p>
</li>
</ol>
<p>Here's how you might implement this combined approach:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Cache for storing reports until they're sent</span>
<span class="hljs-keyword">let</span> reportCache = []
<span class="hljs-keyword">const</span> MAX_CACHE_SIZE = <span class="hljs-number">10</span>
<span class="hljs-keyword">let</span> timer = <span class="hljs-literal">null</span>

<span class="hljs-comment">// Report data with requestIdleCallback when browser is idle</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">lazyReportCache</span>(<span class="hljs-params">data</span>) </span>{
    reportCache.push(data)

    <span class="hljs-comment">// If cache reaches threshold, schedule sending</span>
    <span class="hljs-keyword">if</span> (reportCache.length &gt;= MAX_CACHE_SIZE) {
        <span class="hljs-comment">// Use requestIdleCallback if available, otherwise setTimeout</span>
        <span class="hljs-keyword">const</span> scheduleFn = <span class="hljs-built_in">window</span>.requestIdleCallback || <span class="hljs-built_in">setTimeout</span>

        <span class="hljs-keyword">if</span> (timer) {
            cancelScheduledReport()
        }

        timer = scheduleFn(<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-comment">// Send cached data in bulk</span>
            <span class="hljs-keyword">const</span> dataToSend = [...reportCache]
            reportCache = []
            report({
                <span class="hljs-attr">type</span>: <span class="hljs-string">'batch'</span>,
                <span class="hljs-attr">data</span>: dataToSend,
            })
            timer = <span class="hljs-literal">null</span>
        }, { <span class="hljs-attr">timeout</span>: <span class="hljs-number">2000</span> }) <span class="hljs-comment">// For requestIdleCallback, timeout after 2s</span>
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cancelScheduledReport</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.requestIdleCallback &amp;&amp; timer) {
        <span class="hljs-built_in">window</span>.cancelIdleCallback(timer)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (timer) {
        <span class="hljs-built_in">clearTimeout</span>(timer)
    }
    timer = <span class="hljs-literal">null</span>
}

<span class="hljs-comment">// Report any remaining data when user leaves the page</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setupUnloadReporting</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'beforeunload'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (reportCache.length &gt; <span class="hljs-number">0</span>) {
            <span class="hljs-comment">// Cancel any scheduled reporting</span>
            cancelScheduledReport()

            <span class="hljs-comment">// Send remaining cached data immediately</span>
            report({
                <span class="hljs-attr">type</span>: <span class="hljs-string">'batch'</span>,
                <span class="hljs-attr">data</span>: reportCache,
            }, <span class="hljs-literal">true</span>) <span class="hljs-comment">// true for immediate sending</span>

            reportCache = []
        }
    })
}
</code></pre>
<p>This implementation:</p>
<ol>
<li><p>Collects data in a cache until it reaches a threshold</p>
</li>
<li><p>Uses <code>requestIdleCallback</code> (or <code>setTimeout</code> as fallback) to send data when the browser is idle</p>
</li>
<li><p>Ensures any remaining data is sent when the user leaves the page</p>
</li>
<li><p>Batches multiple reports together to reduce network requests</p>
</li>
</ol>
<p>By combining these methods, you create a robust reporting system that minimizes performance impact while ensuring data reliability.</p>
<h2 id="heading-summary"><strong>Summary</strong></h2>
<p>In this comprehensive guide, we've explored how to build a complete frontend monitoring SDK for collecting and reporting critical application data. Let's recap what we've covered:</p>
<ol>
<li><p><strong>Performance Monitoring</strong></p>
<ul>
<li><p>We implemented methods to capture key web vitals like FP, FCP, LCP, and CLS</p>
</li>
<li><p>We tracked page load events, API request timing, and resource loading metrics</p>
</li>
<li><p>We measured first screen rendering time and frame rates to ensure smooth user experiences</p>
</li>
<li><p>We added support for SPA-specific metrics like Vue router change rendering time</p>
</li>
</ul>
</li>
<li><p><strong>Error Monitoring</strong></p>
<ul>
<li><p>We built systems to capture resource loading errors, JavaScript exceptions, and Promise rejections</p>
</li>
<li><p>We explored how to use sourcemaps to make minified production errors readable</p>
</li>
<li><p>We integrated with framework-specific error handling for Vue applications</p>
</li>
</ul>
</li>
<li><p><strong>User Behavior Tracking</strong></p>
<ul>
<li><p>We implemented tracking for page views, stay duration, and scroll depth</p>
</li>
<li><p>We created methods to monitor user clicks and navigation patterns</p>
</li>
<li><p>We built custom tracking for SPA navigation with Vue Router</p>
</li>
</ul>
</li>
<li><p><strong>Data Reporting</strong></p>
<ul>
<li><p>We developed robust reporting mechanisms using sendBeacon and XMLHttpRequest</p>
</li>
<li><p>We implemented intelligent reporting timing strategies to minimize performance impact</p>
</li>
<li><p>We created batching mechanisms to reduce network requests</p>
</li>
</ul>
</li>
</ol>
<p>Building your own monitoring SDK gives you complete control over what data you collect and how you process it. This approach offers several advantages over third-party solutions:</p>
<ul>
<li><p><strong>Privacy</strong>: You own all the data and can ensure compliance with regulations like GDPR</p>
</li>
<li><p><strong>Performance</strong>: You can optimize the SDK specifically for your application's needs</p>
</li>
<li><p><strong>Customization</strong>: You can add custom metrics unique to your business requirements</p>
</li>
<li><p><strong>Integration</strong>: Your SDK can easily integrate with your existing systems</p>
</li>
</ul>
<p>As you implement your own monitoring solution, remember these best practices:</p>
<ol>
<li><p><strong>Respect User Privacy</strong>: Only collect what you need and be transparent about it</p>
</li>
<li><p><strong>Minimize Performance Impact</strong>: Ensure your monitoring doesn't degrade the user experience</p>
</li>
<li><p><strong>Balance Detail and Volume</strong>: More data isn't always better if it overwhelms your analysis</p>
</li>
<li><p><strong>Act on Insights</strong>: The ultimate goal is to improve your application based on the data</p>
</li>
</ol>
<p>By following the approaches outlined in this article, you'll be well-equipped to build a comprehensive monitoring system that helps you deliver better user experiences through data-driven decision making.</p>
<h2 id="heading-references"><strong>References</strong></h2>
<h3 id="heading-performance-monitoring"><strong>Performance Monitoring</strong></h3>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API">Performance API</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming">PerformanceResourceTiming</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API">Using_the_Resource_Timing_API</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming">PerformanceTiming</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/metrics/">Metrics</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/evolving-cls/">evolving-cls</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/custom-metrics/">custom-metrics</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/GoogleChrome/web-vitals">web-vitals</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver">PerformanceObserver</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Element_timing_API">Element_timing_API</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming">PerformanceEventTiming</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin">Timing-Allow-Origin</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/bfcache/">bfcache</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></p>
</li>
</ul>
<h3 id="heading-error-monitoring"><strong>Error Monitoring</strong></h3>
<ul>
<li><p><a target="_blank" href="https://github.com/joeyguo/noerror">noerror</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/mozilla/source-map">source-map</a></p>
</li>
</ul>
<h3 id="heading-behavior-monitoring"><strong>Behavior Monitoring</strong></h3>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event">popstate</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event">hashchange</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up Grafana on EC2 ]]>
                </title>
                <description>
                    <![CDATA[ In today's data-driven world, it's important to monitor and visualize system metrics to make sure everything works consistently and performs well.  Grafana is an open-source analytics and monitoring platform. It has gained widespread recognition amon... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-set-up-grafana-on-ec2/</link>
                <guid isPermaLink="false">66ba0c6be272700c6e2ec43d</guid>
                
                    <category>
                        <![CDATA[ analytics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Grafana ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Onwubiko Emmanuel ]]>
                </dc:creator>
                <pubDate>Fri, 02 Aug 2024 13:42:27 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/08/pexels-kawserhamid-176342.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In today's data-driven world, it's important to monitor and visualize system metrics to make sure everything works consistently and performs well. </p>
<p>Grafana is an open-source analytics and monitoring platform. It has gained widespread recognition among developers and enterprises looking to extract more insights from the data produced by their systems. </p>
<p>Grafana has many powerful visualization features, and when combined with Amazon EC2's scalability and flexibility, it creates a stable environment for efficient monitoring. </p>
<p>This article will walk you through setting up Grafana on Amazon EC2 and creating informative dashboards out of raw data. </p>
<h2 id="heading-for-whom-is-this-intended"><strong>For Whom is this Intended?</strong></h2>
<p>This tutorial is intended for both novices to the cloud and experts in DevOps. The goal of this post is to make the installation process easier so you can use Grafana on AWS to its fullest. Now let's get going.</p>
<h2 id="heading-how-to-configure-your-ec2-instance"><strong>How to Configure Your EC2 Instance</strong></h2>
<p>You need to configure the inbound rule for your EC2 instance to access port 3000, as Grafana operates on this port. But first, you need to establish an EC2 instance. You can follow this guide on how to set up your <a target="_blank" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html">AWS EC2</a> instance. It takes less than 5 minutes.</p>
<p>Once you have created your EC2 instance, you'll need to configure the network inbound rules. So head to your instance page and click on it. On the button widget, click on the <strong>security</strong> tab and click on the security group link (it should look like this: “<strong>sg-547<strong><strong><strong><strong><em>**</em></strong></strong></strong></strong></strong>”). </p>
<p>Once you open the page in the inbound rules section, click on ‘<strong>Edit inbound rules</strong>’. Click on Add a new rule and add <strong>3000</strong> to the port range field, and on the source field, select <strong>0.0.0.0/0.</strong> Then save.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721347239653_image.png" alt="Image" width="1024" height="162" loading="lazy">
<em>Inbound rules</em></p>
<h2 id="heading-how-to-create-an-iam-role"><strong>How to Create an IAM Role</strong></h2>
<p>Now you need to construct an <strong>IAM (Identity Access Management)</strong> role. You're developing an identity role so that you can generate credentials that you'll subsequently use to log in to your Grafana service.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721348061199_IAM+Dashboard.png" alt="Image" width="1912" height="876" loading="lazy">
<em>IAM Dashboard</em></p>
<p>So, in the search field, type "<strong>IAM service</strong>" and click it. Click '<strong>Create role</strong>', and select the AWS service as the trusted entity type.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721348079999_IAM+Role+creation.png" alt="Image" width="1893" height="865" loading="lazy">
<em>IAM Trusted Entity</em></p>
<p>On the use case section, select EC2, then click next.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721348098668_EC2+Use+Case.png" alt="Image" width="1906" height="877" loading="lazy">
<em>IAM role use case</em></p>
<p>On the Add Permissions page, click on the <strong>AdministratorAccess</strong> policy, then click next. Enter a role name – in this case, I used <strong>Grafana-Server-Role.</strong></p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721348120427_IAM+role+modify+.png" alt="Image" width="1916" height="835" loading="lazy">
<em>Role creation</em></p>
<h2 id="heading-how-to-download-grafana">How to Download Grafana</h2>
<p>Now that you've configured your EC2 inbound rule and also configured the IAM role, let's set up Grafana on your EC2 instance. </p>
<p>So head over to <a target="_blank" href="https://grafana.com/grafana/download">Grafana's download page</a>. Since we'll be downloading the version for Amazon Linux in this tutorial, you need to type in the following command on your Linux command line. Note: You need to connect to your VM instance through SSH (Secure Shell). In this case, I am using the EC2 Instance Connect.</p>
<pre><code class="lang-bash">sudo yum install -y https://dl.grafana.com/enterprise/release/grafana-enterprise-11.1.0-1.x86_64.rpm
</code></pre>
<p>Now you'll enable the Grafana service on your terminal by typing the following command:</p>
<pre><code class="lang-bash">systemctl <span class="hljs-built_in">enable</span> grafana-server.service
</code></pre>
<p>Then start the service:</p>
<pre><code class="lang-bash">systemctl start grafana-server.service
</code></pre>
<p>Check the status of the Grafana service on the EC2 instance by running this command:</p>
<pre><code class="lang-bash">systemctl status grafana-server.service
</code></pre>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721411484886_Grafana+Active+.png" alt="Image" width="1101" height="175" loading="lazy">
<em>Grafana Service Status</em></p>
<p>Now that you've confirmed that the service is currently active, you'll also need to check if the Grafana service is active on <strong>port 3000</strong>, as you've already created an inbound rule to cater for this. </p>
<p>You can do this by typing the following command:</p>
<pre><code class="lang-bash">netstat -tunpl | grep grafana
</code></pre>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721411578753_3000+active.png" alt="Image" width="1107" height="22" loading="lazy">
<em>Port 3000 confirmation</em></p>
<p>Now that you've confirmed that the service runs on port 3000, you can go ahead and set up your Grafana dashboard.</p>
<p>You can access the Grafana dashboard by typing the Public IP of your EC2 instance and adding port 3000 on your web browser, something like this: <strong>34.239.101.172:3000</strong>.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721411871355_Grafana.png" alt="Image" width="1877" height="931" loading="lazy">
<em>Grafana Login</em></p>
<p>The default username and password for Grafana are admin, but you'll be given the option to change your password after you sign in with the default credentials. You can also skip the password change process if you like.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721412805910_Grafana+Password.png" alt="Image" width="1902" height="927" loading="lazy">
<em>Change password on Grafana</em></p>
<p>After this step, go to the home page. The next thing to do is to start connecting your Grafana dashboard to a data source. In this case, you're going to connect it to the AWS cloud watch service.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721413426627_Grafana+home.png" alt="Image" width="1918" height="907" loading="lazy">
<em>Grafana</em></p>
<h2 id="heading-how-to-connect-data-sources-to-the-grafana-dashboard"><strong>How to Connect Data Sources to the Grafana Dashboard</strong></h2>
<p>Click on the connections tab on the side menu and click on data sources. Search for the CloudWatch service.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721413905866_image.png" alt="Image" width="1361" height="646" loading="lazy">
<em>Cloudwatch configuration</em></p>
<p>Now you'll be prompted to input your access key ID and secret access key. You will need to create this on your AWS IAM service. </p>
<p>So go back to your IAM management dashboard and go to the user's tab. If you haven’t created an IAM user, you can do so by checking out this <a target="_blank" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html">IAM user creation tutorial</a>. </p>
<p>In the user IAM dashboard, scroll down to the access keys section and click on <strong>Create access key.</strong></p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721414737862_Access+Key.png" alt="Image" width="1447" height="277" loading="lazy">
<em>Access key</em></p>
<p>Select the Command Line Interface use case.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721414696820_Access+Key+2.png" alt="Image" width="1917" height="876" loading="lazy">
<em>Access key use case</em></p>
<p>Set the description tag. This step is optional. Then click on the <strong>Create access key.</strong></p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721414844084_Access+Key+3.png" alt="Image" width="1787" height="738" loading="lazy">
<em>Access keys</em></p>
<p>Now copy the Access Key ID and Secret access key and paste them into the CloudWatch Datasource configuration page on Grafana. Set your default cloud region – in this case, mine is <strong>us-east-1</strong></p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721414955923_image.png" alt="Image" width="558" height="260" loading="lazy">
<em>Additional settings</em></p>
<p>When you’re done, click on the save and test buttons. Grafana will query the Cloudwatch logs, and if it works fine it will save the configuration.</p>
<h2 id="heading-how-to-create-a-dashboard-on-grafana"><strong>How to Create a Dashboard on Grafana</strong></h2>
<p>Now that you have successfully configured your grafana service, let’s start creating dashboards.</p>
<p>Click on the dashboard tab on the side menu click on <strong>New</strong> and select new dashboard. You should see the screen below:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721416576260_dashboard.png" alt="Image" width="1910" height="928" loading="lazy">
<em>Create a new dashboard</em></p>
<p>Then select <strong>Import dashboard.</strong></p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721417832804_image.png" alt="Image" width="1354" height="643" loading="lazy">
<em>Import a dashboard</em></p>
<p>For this case, you'll be importing an already-made dashboard from Grafana. Grafana has a lot of dashboards for a lot of use cases and services. But in this case, you'll be importing an EC2 dashboard (<a target="_blank" href="https://grafana.com/grafana/dashboards/11265-amazon-ec2/">Grafana EC2 dashboard</a>). </p>
<p>If you want to import it, you can easily copy the ID of the dashboard that you want to import. It is always accompanied by the dashboard.</p>
<p>So now copy the ID – in this case, it's <strong>11265</strong>. Then paste it into the import field on the import dashboard, and click on the load button.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_4B51535633ABB1D019D79F3934180D191EF4BB549B6DD5EF46643EA16E05EAAE_1721418236847_Grafana+Dashboard.png" alt="Image" width="1912" height="927" loading="lazy">
<em>Grafana Dashboard</em></p>
<p>Now you have successfully created a dashboard in Grafana. This dashboard lets you monitor the performance of your EC2 instance. You can monitor metrics such as CPU Utilization, CPU Credit, Disk Ops, Disk Bytes, Network, Network Packets, Status check, and so on.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Thank you for reading! I hope this step by step guide has helped you learn how to create and set up efficient dashboards using Grafana. </p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
