<?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>Sun, 15 Nov 2026 03:20:14 +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[ AWS Cloud Cost Monitoring, Alerting, and Optimization: A Guide for Devs ]]>
                </title>
                <description>
                    <![CDATA[ There's a common conversation that happens in engineering teams every month. Someone forwards a screenshot of the AWS bill. The number is higher than last month. Everyone nods and agrees it should be  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/aws-cloud-cost-monitoring-alerting-and-optimization-a-guide-for-devs/</link>
                <guid isPermaLink="false">6a9ee93ad24de200149086c9</guid>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ optimization ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ayobami Adejumo ]]>
                </dc:creator>
                <pubDate>Mon, 07 Sep 2026 16:41:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/01c5b407-d719-404c-a207-495ae6dcaa53.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>There's a common conversation that happens in engineering teams every month. Someone forwards a screenshot of the AWS bill. The number is higher than last month. Everyone nods and agrees it should be lower. Nothing specific gets decided, and the cycle repeats.</p>
<p>This guide is designed to end that cycle by replacing it with something more useful: a systematic, service-by-service approach to knowing exactly what you're spending, why you're spending it, catching problems before they become invoices, and reducing costs without guesswork.</p>
<p>It's organised as a reference you can return to. Each part is complete on its own: you can go straight to the RDS section if that's your current problem, or follow the guide start to finish if you're building a FinOps practice from scratch. Every command is runnable, and every script is deployable.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-part-1-monitoring-know-where-every-dollar-goes">Part 1: Monitoring — Know Where Every Dollar Goes</a></p>
</li>
<li><p><a href="#heading-part-2-alerting-catch-spikes-before-they-become-invoices">Part 2: Alerting — Catch Spikes Before They Become Invoices</a></p>
</li>
<li><p><a href="#heading-part-3-optimisation-by-service">Part 3: Optimisation by Service</a></p>
</li>
<li><p><a href="#heading-part-4-the-30-day-optimisation-sprint">Part 4: The 30-Day Optimisation Sprint</a></p>
</li>
<li><p><a href="#heading-best-practices-summary">Best Practices Summary</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn">What You'll Learn</h2>
<ul>
<li><p>How to set up Cost and Usage Report querying with Athena, the foundation of all serious cost analysis</p>
</li>
<li><p>The five dashboards every engineering team needs, built with queries you can run today</p>
</li>
<li><p>A three-tier alerting strategy that catches cost spikes without creating alert fatigue</p>
</li>
<li><p>Service-specific optimisation playbooks for EC2, S3, Lambda, RDS, DynamoDB, and data transfer</p>
</li>
<li><p>A concrete 30-day sprint that produces measurable savings in the first month</p>
</li>
</ul>
<p>Let's build this from the ground up.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before following this guide, you should have:</p>
<p><strong>Knowledge:</strong></p>
<ul>
<li><p>Working familiarity with AWS services: EC2, S3, RDS, Lambda, and VPC</p>
</li>
<li><p>Comfort reading Python and SQL</p>
</li>
<li><p>Basic understanding of how IAM policies and roles work</p>
</li>
</ul>
<p><strong>Access:</strong></p>
<ul>
<li><p>AWS account with billing access. The IAM user or role you work with needs <code>ce:GetCostAndUsage</code>, <code>ec2:Describe*</code>, <code>rds:Describe*</code>, and <code>s3:GetBucketLifecycleConfiguration</code> permissions.</p>
</li>
<li><p>AWS CLI v2 configured</p>
</li>
<li><p>Athena access (for the CUR queries in Part 1)</p>
</li>
</ul>
<p><strong>Setup:</strong></p>
<ul>
<li>Enable Cost Explorer if it isn't already. It's free and required for most commands in this guide:</li>
</ul>
<pre><code class="language-bash">aws ce enable-cost-explorer --region us-east-1
</code></pre>
<ul>
<li>The Cost and Usage Report (CUR) should be configured and exporting to an S3 bucket. If it isn't yet, the <a href="https://docs.aws.amazon.com/cur/latest/userguide/cur-create.html">AWS CUR setup guide</a> walks through the process. Give the report 24 hours after setup to generate the first data file.</li>
</ul>
<h2 id="heading-part-1-monitoring-know-where-every-dollar-goes">Part 1: Monitoring — Know Where Every Dollar Goes</h2>
<h3 id="heading-11-the-cost-and-usage-report-your-source-of-truth">1.1 The Cost and Usage Report — Your Source of Truth</h3>
<p>Cost Explorer shows you service-level totals. It's useful for trends, but it isn't sufficient for root cause analysis. When you need to know which specific resource is responsible for a $12,000/month line item, you need the Cost and Usage Report queried through Athena.</p>
<p>Create the Athena table over your CUR data:</p>
<pre><code class="language-sql">-- Run this once in Athena after your CUR starts generating data
-- Replace 'your-cur-bucket' and 'your-prefix' with your actual values

CREATE EXTERNAL TABLE IF NOT EXISTS cur_database.billing (
    bill_billing_period_start_date  STRING,
    bill_payer_account_id           STRING,
    line_item_usage_start_date      STRING,
    line_item_resource_id           STRING,
    line_item_usage_type            STRING,
    line_item_usage_amount          DOUBLE,
    line_item_unblended_cost        DOUBLE,
    product_servicecode             STRING,
    product_instance_type           STRING,
    product_region                  STRING,
    resource_tags_user_environment  STRING,
    resource_tags_user_team         STRING,
    resource_tags_user_service      STRING,
    resource_tags_user_owner        STRING
)
PARTITIONED BY (year STRING, month STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
LOCATION 's3://your-cur-bucket/your-prefix/'
TBLPROPERTIES ('skip.header.line.count'='1');

MSCK REPAIR TABLE cur_database.billing;
</code></pre>
<p>Here are the three queries you should run on day one:</p>
<pre><code class="language-sql">-- Query 1: Top 20 resources by cost this month
-- Run this first. It tells you where to focus.
SELECT
    line_item_resource_id,
    product_servicecode,
    resource_tags_user_team     AS team,
    resource_tags_user_service  AS service,
    SUM(line_item_unblended_cost) AS total_cost_usd
FROM cur_database.billing
WHERE line_item_usage_start_date &gt;= DATE_FORMAT(
    DATE_TRUNC('month', CURRENT_DATE), '%Y-%m-%d'
)
  AND line_item_unblended_cost &gt; 0
GROUP BY 1, 2, 3, 4
ORDER BY total_cost_usd DESC
LIMIT 20;
</code></pre>
<pre><code class="language-sql">-- Query 2: Week-over-week cost growth by service
-- Identifies which services are growing fastest — these need investigation
WITH weekly AS (
    SELECT
        DATE_TRUNC('week', CAST(line_item_usage_start_date AS DATE)) AS week,
        product_servicecode,
        SUM(line_item_unblended_cost) AS cost
    FROM cur_database.billing
    WHERE line_item_usage_start_date &gt;=
          DATE_FORMAT(DATE_ADD('day', -42, CURRENT_DATE), '%Y-%m-%d')
    GROUP BY 1, 2
)
SELECT
    curr.product_servicecode AS service,
    ROUND(prev.cost, 2)      AS prev_week_cost,
    ROUND(curr.cost, 2)      AS curr_week_cost,
    ROUND(
        100.0 * (curr.cost - prev.cost) / NULLIF(prev.cost, 0),
        1
    ) AS pct_change
FROM weekly curr
JOIN weekly prev
  ON curr.product_servicecode = prev.product_servicecode
  AND curr.week = DATE_ADD('week', 1, prev.week)
WHERE curr.week = DATE_TRUNC('week', CURRENT_DATE)
  AND ABS((curr.cost - prev.cost) / NULLIF(prev.cost, 0)) &gt; 0.20
ORDER BY pct_change DESC;
</code></pre>
<pre><code class="language-sql">-- Query 3: Resources running with no usage (candidates for shutdown)
-- Finds resources that incurred cost but had zero usage quantity
-- in the past 7 days — strong signal for idle or orphaned resources
SELECT
    line_item_resource_id,
    product_servicecode,
    resource_tags_user_owner    AS owner,
    resource_tags_user_team     AS team,
    SUM(line_item_unblended_cost) AS cost_past_7_days
FROM cur_database.billing
WHERE line_item_usage_start_date &gt;=
      DATE_FORMAT(DATE_ADD('day', -7, CURRENT_DATE), '%Y-%m-%d')
  AND line_item_usage_amount = 0
  AND line_item_unblended_cost &gt; 5
GROUP BY 1, 2, 3, 4
ORDER BY cost_past_7_days DESC
LIMIT 30;
</code></pre>
<h3 id="heading-12-the-five-essential-cost-dashboards">1.2 The Five Essential Cost Dashboards</h3>
<p>These five views cover the monitoring needs of most engineering teams. Each is built from queries you can run immediately, no third-party tool required.</p>
<h4 id="heading-dashboard-1-executive-summary-for-weekly-leadership-updates">Dashboard 1: Executive Summary (for weekly leadership updates)</h4>
<pre><code class="language-python"># executive_summary.py
import boto3
from datetime import datetime, timedelta

ce = boto3.client('ce')


def weekly_summary():
    today     = datetime.now()
    start_mtd = today.replace(day=1).strftime('%Y-%m-%d')
    today_str = today.strftime('%Y-%m-%d')

    # Month-to-date spend
    mtd = ce.get_cost_and_usage(
        TimePeriod={'Start': start_mtd, 'End': today_str},
        Granularity='MONTHLY',
        Metrics=['UnblendedCost']
    )
    mtd_spend = float(
        mtd['ResultsByTime'][0]['Total']['UnblendedCost']['Amount']
    )

    # End-of-month forecast
    forecast = ce.get_cost_forecast(
        TimePeriod={
            'Start': today_str,
            'End':   (today.replace(day=28) + timedelta(days=4)).replace(day=1).strftime('%Y-%m-%d'),
        },
        Metric='UNBLENDED_COST',
        Granularity='MONTHLY'
    )
    eom_forecast = float(forecast['Total']['Amount']) + mtd_spend

    # Top 5 services
    by_service = ce.get_cost_and_usage(
        TimePeriod={'Start': start_mtd, 'End': today_str},
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}]
    )
    services = sorted(
        [
            (g['Keys'][0], float(g['Metrics']['UnblendedCost']['Amount']))
            for g in by_service['ResultsByTime'][0]['Groups']
        ],
        key=lambda x: x[1],
        reverse=True
    )[:5]

    print(f"\n{'─'*48}")
    print(f"  AWS Cost Summary — {today.strftime('%B %Y')}")
    print(f"{'─'*48}")
    print(f"  Month-to-date:     ${mtd_spend:&gt;12,.2f}")
    print(f"  End-of-month est:  ${eom_forecast:&gt;12,.2f}")
    print(f"\n  Top 5 Services:")
    for name, cost in services:
        short = name.replace('Amazon ', '').replace('AWS ', '')
        print(f"    {short:&lt;32} ${cost:&gt;9,.2f}")
    print(f"{'─'*48}\n")


weekly_summary()
</code></pre>
<h4 id="heading-dashboard-2-team-cost-breakdown-for-engineering-leads">Dashboard 2: Team Cost Breakdown (for engineering leads)</h4>
<pre><code class="language-python"># team_breakdown.py
import boto3
from datetime import datetime

ce = boto3.client('ce')


def team_breakdown():
    start = datetime.now().replace(day=1).strftime('%Y-%m-%d')
    end   = datetime.now().strftime('%Y-%m-%d')

    response = ce.get_cost_and_usage(
        TimePeriod={'Start': start, 'End': end},
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[
            {'Type': 'TAG',       'Key': 'Team'},
            {'Type': 'DIMENSION', 'Key': 'SERVICE'},
        ]
    )

    by_team = {}
    for group in response['ResultsByTime'][0].get('Groups', []):
        team_raw = group['Keys'][0]
        team     = team_raw.replace('Team$', '') if team_raw else 'untagged'
        service  = group['Keys'][1]
        cost     = float(group['Metrics']['UnblendedCost']['Amount'])

        if team not in by_team:
            by_team[team] = {'total': 0.0, 'by_service': {}}
        by_team[team]['total'] += cost
        by_team[team]['by_service'][service] = (
            by_team[team]['by_service'].get(service, 0.0) + cost
        )

    total_bill = sum(d['total'] for d in by_team.values())

    print(f"\n{'─'*58}")
    print(f"  Team Cost Breakdown — MTD {datetime.now().strftime('%Y-%m-%d')}")
    print(f"  Total: ${total_bill:,.2f}")
    print(f"{'─'*58}")

    for team, data in sorted(by_team.items(), key=lambda x: x[1]['total'], reverse=True):
        pct = (data['total'] / total_bill * 100) if total_bill else 0
        print(f"\n  {team:&lt;20}  ${data['total']:&gt;10,.2f}  ({pct:.1f}%)")
        top3 = sorted(data['by_service'].items(), key=lambda x: x[1], reverse=True)[:3]
        for svc, cost in top3:
            short = svc.replace('Amazon ', '').replace('AWS ', '')
            print(f"    └─ {short:&lt;30} ${cost:&gt;8,.2f}")

    print()


team_breakdown()
</code></pre>
<h4 id="heading-dashboard-3-waste-detection-for-weekly-cleanup-reviews">Dashboard 3: Waste Detection (for weekly cleanup reviews):</h4>
<pre><code class="language-python"># waste_detector.py
import boto3
from datetime import datetime, timezone, timedelta

ec2  = boto3.client('ec2')
elbv2 = boto3.client('elbv2')
cw   = boto3.client('cloudwatch')


def detect_waste():
    report = {'items': [], 'total_monthly_waste': 0.0}

    # Unattached EBS volumes
    for vol in ec2.describe_volumes(
        Filters=[{'Name': 'status', 'Values': ['available']}]
    )['Volumes']:
        age  = (datetime.now(timezone.utc) - vol['CreateTime']).days
        cost = round(vol['Size'] * 0.08, 2)
        tags = {t['Key']: t['Value'] for t in vol.get('Tags', [])}
        report['items'].append({
            'type':         'Unattached EBS Volume',
            'id':           vol['VolumeId'],
            'detail':       f"{vol['Size']}GB — {age} days old",
            'owner':        tags.get('Owner', '—'),
            'monthly_cost': cost,
        })
        report['total_monthly_waste'] += cost

    # Unassociated Elastic IPs
    for addr in ec2.describe_addresses()['Addresses']:
        if 'AssociationId' not in addr:
            report['items'].append({
                'type':         'Unassociated Elastic IP',
                'id':           addr.get('AllocationId', ''),
                'detail':       addr['PublicIp'],
                'owner':        '—',
                'monthly_cost': 3.60,
            })
            report['total_monthly_waste'] += 3.60

    # Idle load balancers (fewer than 100 requests in 7 days)
    for lb in elbv2.describe_load_balancers()['LoadBalancers']:
        metrics = cw.get_metric_statistics(
            Namespace='AWS/ApplicationELB',
            MetricName='RequestCount',
            Dimensions=[{'Name': 'LoadBalancer',
                         'Value': lb['LoadBalancerArn'].split(':loadbalancer/')[-1]}],
            StartTime=datetime.now() - timedelta(days=7),
            EndTime=datetime.now(),
            Period=604800,
            Statistics=['Sum']
        )['Datapoints']
        total_requests = metrics[0]['Sum'] if metrics else 0

        if total_requests &lt; 100:
            report['items'].append({
                'type':         'Idle Load Balancer',
                'id':           lb['LoadBalancerName'],
                'detail':       f"{int(total_requests)} requests in 7 days",
                'owner':        '—',
                'monthly_cost': 22.0,
            })
            report['total_monthly_waste'] += 22.0

    print(f"\n  Waste Detection Report — {datetime.now().strftime('%Y-%m-%d')}")
    print(f"  Estimated monthly waste: ${report['total_monthly_waste']:.2f}\n")

    for item in sorted(report['items'], key=lambda x: x['monthly_cost'], reverse=True)[:20]:
        print(f"  [{item['type']}]")
        print(f"    ID:     {item['id']}")
        print(f"    Detail: {item['detail']}")
        print(f"    Owner:  {item['owner']}")
        print(f"    Cost:   ${item['monthly_cost']:.2f}/month\n")

    return report


detect_waste()
</code></pre>
<h3 id="heading-13-tagging-strategy-the-foundation-of-all-attribution">1.3 Tagging Strategy — The Foundation of All Attribution</h3>
<p>Every cost attribution model depends on tags. Teams that skip tagging build dashboards that show totals without explanations. The discipline is in making tagging structural rather than procedural: enforced by infrastructure code, not by reminders in Confluence.</p>
<p>Required tag set:</p>
<pre><code class="language-hcl"># terraform/variables.tf
variable "mandatory_tags" {
  description = "Tags applied to every resource in this account"
  type        = map(string)

  validation {
    condition = alltrue([
      contains(keys(var.mandatory_tags), "Environment"),
      contains(keys(var.mandatory_tags), "Team"),
      contains(keys(var.mandatory_tags), "Owner"),
      contains(keys(var.mandatory_tags), "Service"),
    ])
    error_message = "mandatory_tags must include Environment, Team, Owner, and Service."
  }
}

locals {
  common_tags = merge(var.mandatory_tags, {
    ManagedBy    = "terraform"
    LastModified = timestamp()
  })
}

resource "aws_instance" "api_server" {
  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = "t3.medium"
  tags          = merge(local.common_tags, {Name = "api-server-${var.environment}"})
}
</code></pre>
<p>Find and report untagged resources weekly:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
# find_untagged.sh

echo "Untagged EC2 instances (missing Team tag):"
aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --query "Reservations[].Instances[?!not_null(Tags[?Key=='Team'].Value|[0])].[InstanceId,InstanceType,LaunchTime]" \
  --output table

echo "Untagged RDS instances:"
aws rds describe-db-instances \
  --query "DBInstances[?!not_null(TagList[?Key=='Team'].Value|[0])].DBInstanceIdentifier" \
  --output table
</code></pre>
<h2 id="heading-part-2-alerting-catch-spikes-before-they-become-invoices">Part 2: Alerting — Catch Spikes Before They Become Invoices</h2>
<p>The typical discovery timeline without proactive alerting: a cost spike happens on the 5th, the monthly invoice arrives on the 20th, someone notices on the 22nd, investigation begins on the 23rd, and two weeks of billed waste can't be recovered. With proactive alerting, discovery happens within hours.</p>
<h3 id="heading-21-the-three-tier-alert-structure">2.1 The Three-Tier Alert Structure</h3>
<p>Alert fatigue is as damaging as no alerting. The three-tier model keeps signal high by routing different severity levels to different channels with different response expectations.</p>
<pre><code class="language-python"># alert_router.py
import boto3
import json
import urllib.request
from enum import Enum

SLACK_INFO_WEBHOOK  = 'https://hooks.slack.com/services/INFO/WEBHOOK'
SLACK_ALERT_WEBHOOK = 'https://hooks.slack.com/services/ALERT/WEBHOOK'
SNS_CRITICAL_TOPIC  = 'arn:aws:sns:us-east-1:YOUR_ACCOUNT:cost-critical'


class AlertTier(Enum):
    INFO     = 1
    WARNING  = 2
    CRITICAL = 3


def route_alert(tier: AlertTier, subject: str, message: str):
    """Send an alert to the appropriate channel for its severity tier."""
    icons   = {AlertTier.INFO: ':information_source:',
               AlertTier.WARNING: ':warning:', AlertTier.CRITICAL: ':rotating_light:'}
    payload = {'text': f"{icons[tier]} *{subject}*\n{message}"}

    if tier == AlertTier.INFO:
        _post_slack(SLACK_INFO_WEBHOOK, payload)
    elif tier == AlertTier.WARNING:
        _post_slack(SLACK_ALERT_WEBHOOK, payload)
        _send_sns(SNS_CRITICAL_TOPIC, subject, f"WARNING: {message}")
    elif tier == AlertTier.CRITICAL:
        _post_slack(SLACK_ALERT_WEBHOOK, payload)
        _send_sns(SNS_CRITICAL_TOPIC, subject, f"CRITICAL: {message}")


def _post_slack(webhook: str, payload: dict):
    req = urllib.request.Request(
        webhook,
        data=json.dumps(payload).encode(),
        headers={'Content-Type': 'application/json'}
    )
    urllib.request.urlopen(req)


def _send_sns(topic_arn: str, subject: str, message: str):
    sns = boto3.client('sns')
    sns.publish(TopicArn=topic_arn, Subject=subject[:100], Message=message)
</code></pre>
<p>The three tiers and what they respond to: Tier 1 INFO goes to a Slack informational channel for daily cost summaries, weekly trend reports, and tag compliance updates. No action required.</p>
<p>Tier 2 WARNING goes to a Slack alert channel plus email for budget above 75% utilisation, 25% week-over-week increases, and expiring Savings Plans. Acknowledge within 24 hours.</p>
<p>Tier 3 CRITICAL goes to PagerDuty plus SMS for budget above 90% utilisation, 100% increase in 24 hours, crypto mining detected, and projected overspend above 120% of plan. Investigate within 1 hour.</p>
<h3 id="heading-22-real-time-budget-monitor">2.2 Real-Time Budget Monitor</h3>
<p>AWS Budgets sends alerts once daily by default. A daily window means a cost spike that begins at 08:00 isn't caught until the next day's alert fires. The Lambda below runs hourly and checks both absolute budget utilisation and hour-over-hour rate of change.</p>
<pre><code class="language-python"># budget_monitor.py
# Lambda triggered by EventBridge every hour

import boto3
from datetime import datetime, timedelta
from alert_router import route_alert, AlertTier

ce      = boto3.client('ce')
budgets = boto3.client('budgets', region_name='us-east-1')
ACCOUNT_ID  = boto3.client('sts').get_caller_identity()['Account']
BUDGET_NAME = 'monthly-infrastructure'


def get_mtd_spend() -&gt; float:
    start = datetime.now().replace(day=1).strftime('%Y-%m-%d')
    end   = datetime.now().strftime('%Y-%m-%d')
    r = ce.get_cost_and_usage(
        TimePeriod={'Start': start, 'End': end},
        Granularity='MONTHLY',
        Metrics=['UnblendedCost']
    )
    return float(r['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])


def get_budget_limit() -&gt; float:
    r = budgets.describe_budget(AccountId=ACCOUNT_ID, BudgetName=BUDGET_NAME)
    return float(r['Budget']['BudgetLimit']['Amount'])


def get_hourly_costs(hours: int = 4) -&gt; list:
    """Return hourly cost totals for the last N hours."""
    end   = datetime.now()
    start = end - timedelta(hours=hours)
    r = ce.get_cost_and_usage(
        TimePeriod={'Start': start.strftime('%Y-%m-%d'), 'End': end.strftime('%Y-%m-%d')},
        Granularity='HOURLY',
        Metrics=['UnblendedCost']
    )
    return [
        float(period['Total']['UnblendedCost']['Amount'])
        for period in r['ResultsByTime']
    ]


def lambda_handler(event, context):
    mtd_spend    = get_mtd_spend()
    budget_limit = get_budget_limit()
    utilisation  = mtd_spend / budget_limit * 100

    days_elapsed  = datetime.now().day
    projected_eom = (mtd_spend / days_elapsed) * 30
    projected_pct = projected_eom / budget_limit * 100

    if utilisation &gt;= 90:
        route_alert(
            AlertTier.CRITICAL,
            f'Budget at {utilisation:.0f}%',
            f'MTD spend ${mtd_spend:,.2f} is {utilisation:.0f}% of ${budget_limit:,.0f} budget. '
            f'Projected EOM: ${projected_eom:,.2f}.'
        )
    elif utilisation &gt;= 75:
        route_alert(
            AlertTier.WARNING,
            f'Budget at {utilisation:.0f}%',
            f'MTD spend ${mtd_spend:,.2f} is {utilisation:.0f}% of ${budget_limit:,.0f} budget. '
            f'Projected EOM: ${projected_eom:,.2f}.'
        )

    # Check hourly spike
    hourly = get_hourly_costs(hours=4)
    if len(hourly) &gt;= 2:
        last_hour = hourly[-1]
        prev_avg  = sum(hourly[:-1]) / len(hourly[:-1])
        if prev_avg &gt; 0.10 and last_hour &gt; prev_avg * 1.5:
            route_alert(
                AlertTier.WARNING,
                'Hourly cost spike detected',
                f'Last hour: ${last_hour:.2f} vs prior 3-hour avg ${prev_avg:.2f} '
                f'(+{(last_hour/prev_avg - 1)*100:.0f}%)'
            )

    return {
        'mtd_spend':       round(mtd_spend, 2),
        'utilisation_pct': round(utilisation, 1),
        'projected_eom':   round(projected_eom, 2),
    }
</code></pre>
<h2 id="heading-part-3-optimisation-by-service">Part 3: Optimisation by Service</h2>
<h3 id="heading-31-ec2-seven-levers-in-priority-order">3.1 EC2 — Seven Levers in Priority Order</h3>
<p>EC2 is the largest line item in most AWS accounts and the one with the most optimisation options. Work through these levers in order, as each one lowers the baseline that the next lever acts on.</p>
<p>Lever 1: Find truly idle instances (CPU below 1% for 14 days).</p>
<pre><code class="language-python"># ec2_idle_finder.py
import boto3
from datetime import datetime, timedelta

ec2 = boto3.client('ec2')
cw  = boto3.client('cloudwatch')


def find_idle_instances(avg_cpu_threshold: float = 1.0, days: int = 14):
    instances = [
        inst
        for r in ec2.describe_instances(
            Filters=[{'Name': 'instance-state-name', 'Values': ['running']}]
        )['Reservations']
        for inst in r['Instances']
    ]

    idle = []
    for inst in instances:
        iid   = inst['InstanceId']
        stats = cw.get_metric_statistics(
            Namespace='AWS/EC2',
            MetricName='CPUUtilization',
            Dimensions=[{'Name': 'InstanceId', 'Value': iid}],
            StartTime=datetime.utcnow() - timedelta(days=days),
            EndTime=datetime.utcnow(),
            Period=days * 86400,
            Statistics=['Average']
        )['Datapoints']

        avg_cpu = stats[0]['Average'] if stats else 0.0
        if avg_cpu &lt; avg_cpu_threshold:
            tags = {t['Key']: t['Value'] for t in inst.get('Tags', [])}
            idle.append({
                'instance_id':   iid,
                'instance_type': inst['InstanceType'],
                'avg_cpu':       round(avg_cpu, 2),
                'environment':   tags.get('Environment', '—'),
                'owner':         tags.get('Owner', '—'),
            })

    return sorted(idle, key=lambda x: x['avg_cpu'])


for inst in find_idle_instances():
    print(f"  {inst['instance_id']}  {inst['instance_type']}  "
          f"{inst['avg_cpu']}% CPU  env:{inst['environment']}  owner:{inst['owner']}")
</code></pre>
<p>Lever 2: Right-size over-provisioned instances (CPU below 20%, sustained).</p>
<p>Use the same script with <code>avg_cpu_threshold=20.0</code>. These are right-sizing candidates, not shutdown candidates.</p>
<p>Lever 3: Schedule dev and staging shutdowns using EventBridge rules on <code>AutoShutdown=true</code> tagged instances.</p>
<p>Lever 4: Purchase Savings Plans only after completing levers 1–3.</p>
<p>Lever 5: Migrate to Graviton (20% cheaper, same performance for most workloads).</p>
<p>Lever 6: Use Spot for fault-tolerant batch and development workloads.</p>
<p>Lever 7: Migrate containerised workloads to EKS with Karpenter for automatic bin-packing.</p>
<p>Spot savings estimate:</p>
<pre><code class="language-python"># spot_price_analyser.py
import boto3

ec2 = boto3.client('ec2')


def spot_savings_estimate(instance_type: str) -&gt; dict:
    spot_history = ec2.describe_spot_price_history(
        InstanceTypes=[instance_type],
        ProductDescriptions=['Linux/UNIX'],
        MaxResults=1
    )['SpotPriceHistory']
    spot_price = float(spot_history[0]['SpotPrice']) if spot_history else 0

    on_demand_approx = {
        't3.medium': 0.0416, 'm5.large': 0.096,
        'c5.xlarge': 0.17,   'r5.2xlarge': 0.504,
    }
    od_price    = on_demand_approx.get(instance_type, 0)
    savings_pct = ((od_price - spot_price) / od_price * 100) if od_price else 0

    return {
        'instance_type': instance_type,
        'spot_price':    round(spot_price, 4),
        'on_demand':     od_price,
        'savings_pct':   round(savings_pct, 1),
        'monthly_spot':  round(spot_price * 730, 2),
        'monthly_od':    round(od_price * 730, 2),
    }


for itype in ['t3.medium', 'm5.large', 'c5.xlarge']:
    r = spot_savings_estimate(itype)
    print(f"  {r['instance_type']:&lt;15} Spot: ${r['spot_price']}/hr  "
          f"OD: ${r['on_demand']}/hr  Savings: {r['savings_pct']}%")
</code></pre>
<h3 id="heading-32-s3-lifecycle-policies-and-storage-class-selection">3.2 S3 — Lifecycle Policies and Storage Class Selection</h3>
<p>S3 optimisation has two components: moving infrequently accessed data to cheaper storage classes via lifecycle policies, and eliminating waste patterns like incomplete multipart uploads.</p>
<pre><code class="language-python"># s3_lifecycle_applier.py
import boto3

s3 = boto3.client('s3')

LOG_POLICY = {
    'Rules': [{
        'ID': 'standard-tiering',
        'Status': 'Enabled',
        'Filter': {'Prefix': ''},
        'Transitions': [
            {'Days': 30,  'StorageClass': 'STANDARD_IA'},
            {'Days': 90,  'StorageClass': 'GLACIER_IR'},
            {'Days': 365, 'StorageClass': 'DEEP_ARCHIVE'},
        ],
        'Expiration': {'Days': 2555},
        'AbortIncompleteMultipartUpload': {'DaysAfterInitiation': 7},
    }]
}

TEMP_POLICY = {
    'Rules': [{
        'ID': 'temp-data-retention',
        'Status': 'Enabled',
        'Filter': {'Prefix': ''},
        'Expiration': {'Days': 30},
        'AbortIncompleteMultipartUpload': {'DaysAfterInitiation': 1},
    }]
}

for bucket in s3.list_buckets()['Buckets']:
    name = bucket['Name']
    try:
        s3.get_bucket_lifecycle_configuration(Bucket=name)
        print(f"  {name} — policy already exists, skipping")
    except s3.exceptions.ClientError:
        policy = TEMP_POLICY if any(k in name for k in ['temp', 'build', 'cache']) else LOG_POLICY
        s3.put_bucket_lifecycle_configuration(
            Bucket=name, LifecycleConfiguration=policy
        )
        print(f"  {name} — applied {'TEMP' if policy is TEMP_POLICY else 'LOG'} policy")
</code></pre>
<h3 id="heading-33-rds-five-optimisation-levels">3.3 RDS — Five Optimisation Levels</h3>
<table>
<thead>
<tr>
<th>Level</th>
<th>Action</th>
<th>Typical Saving</th>
<th>Risk</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Delete unused read replicas</td>
<td>30–50% of replica cost</td>
<td>Low</td>
</tr>
<tr>
<td>2</td>
<td>Reduce backup retention to compliance minimum</td>
<td>20–30% of storage cost</td>
<td>Low</td>
</tr>
<tr>
<td>3</td>
<td>Right-size instance class (CPU below 20% sustained)</td>
<td>20–40% of compute</td>
<td>Medium</td>
</tr>
<tr>
<td>4</td>
<td>Purchase Reserved Instances for production</td>
<td>30–60% of compute</td>
<td>Low</td>
</tr>
<tr>
<td>5</td>
<td>Migrate variable-load DBs to Aurora Serverless v2</td>
<td>40–70% total</td>
<td>High effort</td>
</tr>
</tbody></table>
<p>Find over-provisioned RDS instances:</p>
<pre><code class="language-python"># rds_rightsizer.py
import boto3
from datetime import datetime, timedelta

rds = boto3.client('rds')
cw  = boto3.client('cloudwatch')


def find_oversized_rds():
    instances  = rds.describe_db_instances()['DBInstances']
    candidates = []

    for inst in instances:
        iid    = inst['DBInstanceIdentifier']
        iclass = inst['DBInstanceClass']

        stats = cw.get_metric_statistics(
            Namespace='AWS/RDS',
            MetricName='CPUUtilization',
            Dimensions=[{'Name': 'DBInstanceIdentifier', 'Value': iid}],
            StartTime=datetime.utcnow() - timedelta(days=14),
            EndTime=datetime.utcnow(),
            Period=1209600,
            Statistics=['Average', 'Maximum']
        )['Datapoints']

        if not stats:
            continue

        avg_cpu = stats[0]['Average']
        max_cpu = stats[0]['Maximum']

        if avg_cpu &lt; 20 and max_cpu &lt; 50:
            candidates.append({
                'id':       iid,
                'class':    iclass,
                'avg_cpu':  round(avg_cpu, 1),
                'max_cpu':  round(max_cpu, 1),
                'engine':   inst['Engine'],
            })

    return candidates


for c in find_oversized_rds():
    print(f"  {c['id']}  {c['class']}  avg:{c['avg_cpu']}%  max:{c['max_cpu']}%  engine:{c['engine']}")
</code></pre>
<h3 id="heading-34-dynamodb-on-demand-vs-provisioned-decision">3.4 DynamoDB — On-Demand vs Provisioned Decision</h3>
<p>On-demand is convenient but can be 3–5× more expensive than provisioned for predictable workloads. Provisioned with auto-scaling covers most cases at significantly lower cost.</p>
<pre><code class="language-python"># dynamodb_mode_advisor.py
import boto3
from datetime import datetime, timedelta

dynamodb = boto3.client('dynamodb')
cw       = boto3.client('cloudwatch')


def analyse_table_billing(table_name: str) -&gt; dict:
    table = dynamodb.describe_table(TableName=table_name)['Table']
    mode  = table.get('BillingModeSummary', {}).get('BillingMode', 'PROVISIONED')

    stats = {}
    for metric in ['ConsumedReadCapacityUnits', 'ConsumedWriteCapacityUnits']:
        data = cw.get_metric_statistics(
            Namespace='AWS/DynamoDB',
            MetricName=metric,
            Dimensions=[{'Name': 'TableName', 'Value': table_name}],
            StartTime=datetime.utcnow() - timedelta(days=30),
            EndTime=datetime.utcnow(),
            Period=86400,
            Statistics=['Average', 'Maximum']
        )['Datapoints']
        if data:
            avg  = sum(d['Average'] for d in data) / len(data)
            peak = max(d['Maximum'] for d in data)
            stats[metric] = {'avg': round(avg, 1), 'peak': round(peak, 1)}

    if mode == 'PAY_PER_REQUEST':
        avg_rcu = stats.get('ConsumedReadCapacityUnits', {}).get('avg', 0)
        avg_wcu = stats.get('ConsumedWriteCapacityUnits', {}).get('avg', 0)

        if avg_rcu &gt; 2000 or avg_wcu &gt; 500:
            return {
                'table':          table_name,
                'current_mode':   'PAY_PER_REQUEST',
                'recommendation': 'Switch to PROVISIONED with auto-scaling',
                'reason': f'Avg {avg_rcu:.0f} RCU/s and {avg_wcu:.0f} WCU/s — predictable pattern',
            }

    return {'table': table_name, 'current_mode': mode, 'recommendation': 'No change needed'}


for table in dynamodb.list_tables()['TableNames']:
    r = analyse_table_billing(table)
    if r['recommendation'] != 'No change needed':
        print(f"  {r['table']}: {r['recommendation']}")
</code></pre>
<h3 id="heading-35-data-transfer-the-three-main-waste-patterns">3.5 Data Transfer — The Three Main Waste Patterns</h3>
<p>Data transfer charges are often the most confusing line item on an AWS bill. Here are the the three main patterns and their fixes:</p>
<p>Cross-AZ traffic (most common, most fixable): services in different AZs incur $0.01/GB in each direction. The fix is to use topology-aware routing on Kubernetes Services or ensure your application tier and database tier use the same AZ placement.</p>
<p>NAT Gateway charges for internal AWS traffic: S3, ECR, DynamoDB, and SQS traffic that routes through NAT Gateway incurs $0.045/GB. The fix: VPC endpoints eliminate this entirely.</p>
<p>Inter-region replication that compliance doesn't require: audit your S3 replication rules quarterly against actual compliance requirements.</p>
<pre><code class="language-bash"># Find S3 buckets with active replication
for bucket in $(aws s3api list-buckets --query 'Buckets[*].Name' --output text); do
    result=$(aws s3api get-bucket-replication --bucket "$bucket" 2&gt;&amp;1)
    if ! echo "$result" | grep -q "ReplicationConfigurationNotFoundError"; then
        echo "  $bucket — replication active, verify compliance requirement"
    fi
done
</code></pre>
<h2 id="heading-part-4-the-30-day-optimisation-sprint">Part 4: The 30-Day Optimisation Sprint</h2>
<p>This sprint produces measurable savings in the first month. It's designed for a single engineer with two to four hours per week of dedicated FinOps time.</p>
<p>Week 1 – Visibility: enable CUR, set up the Athena table, run the three day-one queries, screenshot the results as your baseline, tag 100% of running EC2 and RDS instances, and identify your top three cost drivers with a documented hypothesis for each.</p>
<p>Week 2 – Quick wins: deploy the orphaned resource reporter Lambda, apply S3 lifecycle policies to your three largest buckets, run the idle instance finder and stop anything below 1% average CPU with no owner objection, and add VPC endpoints for S3, ECR, and DynamoDB.</p>
<p>Week 3 – Right-sizing: run the EC2 rightsizing analyser, downsize the three highest-confidence candidates (non-production first), run the RDS rightsizing script, and find read replicas serving minimal traffic and decommission them.</p>
<p>Week 4 – Alerting and automation: deploy the hourly budget monitor Lambda, configure the three-tier alert routing, set up the weekly waste reporter, add the Infracost GitHub Action to your infrastructure repository, and schedule a monthly 30-minute FinOps review meeting.</p>
<p>Expected outcome after 30 days: 15–25% reduction in monthly AWS spend, documented evidence of every change, and a recurring process that prevents the same waste from accumulating again.</p>
<h2 id="heading-best-practices-summary">Best Practices Summary</h2>
<p>✅ <strong>Do:</strong> Set up CUR + Athena before any other monitoring. Cost Explorer is a starting point, while CUR is the source of truth.</p>
<p>✅ <strong>Do:</strong> Enforce tagging in Terraform or CloudFormation. Process-based tagging decays, but infrastructure-enforced tagging is permanent.</p>
<p>✅ <strong>Do:</strong> Run the idle instance finder and waste reporter weekly. Waste accumulates continuously. A weekly report keeps the pile small.</p>
<p>✅ <strong>Do:</strong> Use the three-tier alert model. One alert channel with everything in it creates fatigue and gets muted.</p>
<p>✅ <strong>Do:</strong> Work through the EC2 optimisation levers in order. Right-sizing before Savings Plans prevents locking in waste at a discount.</p>
<p>✅ <strong>Do:</strong> Check DynamoDB billing mode against actual usage patterns quarterly.</p>
<p>❌ <strong>Don't:</strong> Delete untagged resources without investigation. Untagged doesn't mean unused, it means unclaimed.</p>
<p>❌ <strong>Don't:</strong> Apply aggressive S3 lifecycle policies without auditing access patterns first. Glacier retrieval fees can exceed Standard storage costs if data is accessed more frequently than expected.</p>
<p>❌ <strong>Don't:</strong> Run the waste reporter Lambda with auto-deletion enabled on its first deployment. Run in report-only mode for two weeks to validate the output before adding deletion logic.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://docs.aws.amazon.com/cur/latest/userguide/data-dictionary.html"><strong>AWS Cost and Usage Report Data Dictionary</strong></a>: Column reference for all CUR Athena queries in this guide</p>
</li>
<li><p><a href="https://docs.aws.amazon.com/cost-management/latest/APIReference/"><strong>AWS Cost Explorer API Reference</strong></a>: Full reference for the Python boto3 cost queries</p>
</li>
<li><p><a href="https://aws.amazon.com/compute-optimizer/"><strong>AWS Compute Optimizer</strong></a>: ML-powered right-sizing recommendations, useful as a cross-check against the manual analyser scripts</p>
</li>
<li><p><a href="https://aws.amazon.com/dynamodb/pricing/"><strong>Amazon DynamoDB Pricing</strong></a>: The definitive reference for the provisioned vs on-demand cost calculation in Section 3.4</p>
</li>
<li><p><a href="https://aws.amazon.com/solutions/implementations/instance-scheduler-on-aws/"><strong>AWS Instance Scheduler</strong></a>: The official AWS solution for tag-based EC2 and RDS scheduling</p>
</li>
<li><p><a href="https://www.finops.org/framework/"><strong>FinOps Foundation Framework</strong></a>: The practitioner framework that defines the Inform, Optimise, Operate cycle this guide implements</p>
</li>
<li><p><a href="https://github.com/aayostem/platform-toolkit"><strong>Companion Repository</strong></a>: All scripts, Lambda functions, and Terraform modules from this guide</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How OpenTelemetry Works: A Complete Guide ]]>
                </title>
                <description>
                    <![CDATA[ If you’re a software developer or DevOps engineer, you've probably come across OpenTelemetry. It comes up a lot, especially when talking about observability, monitoring, or debugging distributed syste ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-opentelemetry-works/</link>
                <guid isPermaLink="false">6a999540c11d1e5beaccfefc</guid>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ distributed tracing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloud native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OTLP ]]>
                    </category>
                
                    <category>
                        <![CDATA[ opentelemetry collector ]]>
                    </category>
                
                    <category>
                        <![CDATA[ metrics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Logs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ traces ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chosen Vincent ]]>
                </dc:creator>
                <pubDate>Thu, 03 Sep 2026 15:41:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/32307170-27b3-463c-bae9-da3dbfd2a634.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you’re a software developer or DevOps engineer,&nbsp;you've probably come across OpenTelemetry. It comes up a lot, especially when talking about observability, monitoring, or debugging distributed systems.</p>
<p>You might even know the basic definition, but knowing what OpenTelemetry is vs how it actually works are two different things.</p>
<p>By the end of this guide, you'll understand how OpenTelemetry works end-to-end, from the moment a request enters your application to the moment you can see it in your observability backend. You'll learn how traces, spans, context propagation, and exporters all fit together into one pipeline.</p>
<p>If you're completely new to OpenTelemetry, don't worry: the next section will get you up to speed before we go any further.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-opentelemetry">What is OpenTelemetry?</a></p>
</li>
<li><p><a href="#heading-how-opentelemetry-works">How OpenTelemetry Works</a></p>
<ul>
<li><p><a href="#heading-step-1-instrument-your-application">Step 1: Instrument Your Application</a></p>
</li>
<li><p><a href="#heading-step-2-opentelemetry-creates-telemetry-signals">Step 2: OpenTelemetry Creates Telemetry Signals</a></p>
</li>
<li><p><a href="#heading-step-3-traces-follow-requests-through-your-application">Step 3: Traces Follow Requests Through Your Application</a></p>
</li>
<li><p><a href="#heading-step-4-context-propagation-connects-work-across-services">Step 4: Context Propagation Connects Work Across Services</a></p>
</li>
<li><p><a href="#heading-step-5-the-opentelemetry-sdk-processes-the-telemetry">Step 5: The OpenTelemetry SDK Processes the Telemetry</a></p>
</li>
<li><p><a href="#heading-step-6-exporters-send-the-telemetry">Step 6: Exporters Send the Telemetry</a></p>
</li>
<li><p><a href="#heading-step-7-the-opentelemetry-collector-receives-and-processes-the-data">Step 7: The OpenTelemetry Collector Receives and Processes the Data</a></p>
</li>
<li><p><a href="#heading-step-8-an-observability-backend-stores-and-analyzes-the-telemetry">Step 8: An Observability Backend Stores and Analyzes the Telemetry</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-putting-the-opentelemetry-flow-together">Putting the OpenTelemetry Flow Together</a></p>
</li>
<li><p><a href="#heading-do-you-need-every-opentelemetry-component">Do You Need Every OpenTelemetry Component?</a></p>
</li>
<li><p><a href="#heading-why-use-opentelemetry">Why Use OpenTelemetry?</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-opentelemetry">What is OpenTelemetry?</h2>
<p><a href="https://opentelemetry.io/docs/">OpenTelemetry</a> is an open-source, vendor-neutral observability framework. It gives you a standard way to instrument your application, generate telemetry data, and export that data to any observability backend of your choice.</p>
<p>Before OpenTelemetry, every monitoring tool had its own way of collecting data. If you used Datadog, you’ll have to instrument your app the Datadog way. If you switched to Jaeger, you started over. OpenTelemetry changed that by giving you one standard way to instrument your application, regardless of which backend you use</p>
<p>[!NOTE] OpenTelemetry is not a monitoring platform, dashboard, or data store. It provides the tools and standards for collecting and exporting telemetry from your application to an observability backend, where the data can be stored, queried, and analyzed.</p>
<p>The data OpenTelemetry collects is called telemetry. It's the information your application produces about itself as it runs, and it comes in three forms:</p>
<ul>
<li><p><strong>Traces</strong> tell you how a request traveled through your system.</p>
</li>
<li><p><strong>Metrics</strong> give you numbers, like how many requests per second your app is handling, or how much memory it's using.</p>
</li>
<li><p><strong>Logs</strong> are timestamped records of specific events that happened inside your application.</p>
</li>
</ul>
<h2 id="heading-how-opentelemetry-works">How OpenTelemetry Works</h2>
<p>When a request hits your application, a lot happens behind the scenes. OpenTelemetry's job is to capture all of that activity (the traces, metrics, and logs) and send them to the right place.</p>
<p>Here’s what it looks like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/238d33f9-2a15-49b4-993b-43c073a48305.png" alt="A flow diagram showing the six stages of the OpenTelemetry pipeline: the application is instrumented, telemetry is processed by the SDK, sent through an exporter, received by the Collector, and finally stored in an observability backend." style="display: block;" width="1600" height="400" loading="lazy">

<p>Each stage has a specific job. Let's walk through them one by one.</p>
<h3 id="heading-step-1-instrument-your-application">Step 1: Instrument Your Application</h3>
<p>Before OpenTelemetry can capture anything, your application needs to be instrumented. Instrumentation is simply the process of adding code that tells OpenTelemetry what to watch and what to record.</p>
<p>There are two ways to instrument your application: automatically or manually.</p>
<h4 id="heading-1-automatic-instrumentation">1. Automatic instrumentation</h4>
<p>This is the easiest one to start with. You add a library to your project, and it instruments your application for you with no changes to your existing code.</p>
<p>For example, if you're running a Node.js Express app, you can add the OpenTelemetry auto-instrumentation package, and it will automatically start capturing incoming HTTP requests, outgoing calls, database queries, and more.</p>
<p>Here's what that setup looks like:</p>
<pre><code class="language-javascript">const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
&nbsp; instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
</code></pre>
<p>Once this runs before your app starts, OpenTelemetry begins capturing telemetry automatically. For a complete setup guide, see <a href="https://opentelemetry.io/docs/languages/js/getting-started/nodejs/">Getting started with OpenTelemetry in Node.js</a>.</p>
<h4 id="heading-2-manual-instrumentation">2. Manual instrumentation</h4>
<p>Automatic instrumentation covers a lot, but it can't capture everything that happens inside your own code. If you want to track what happens inside a specific function, like how long it takes to process a payment or validate a user, you need to add that yourself.</p>
<p>Here's a simple example. Let’s say you have a function that processes an order:</p>
<pre><code class="language-javascript">function processOrder(orderId) {
  // processing logic
}
</code></pre>
<p>With manual instrumentation, you wrap it like this:</p>
<pre><code class="language-javascript">const { trace } = require('@opentelemetry/api');

const tracer = trace.getTracer('order-service');

function processOrder(orderId) {
  return tracer.startActiveSpan('processOrder', (span) =&gt; {
    // processing logic

    span.end();
  });
}
</code></pre>
<p>What happens is that you created a span. That span now records when <code>processOrder</code> started, when it ended, and how long it took. You'll learn more about spans in Step 3.</p>
<p>For the full manual instrumentation reference, see <a href="https://opentelemetry.io/docs/languages/js/instrumentation/#traces">OpenTelemetry JavaScript instrumentation</a>.</p>
<h3 id="heading-step-2-opentelemetry-creates-telemetry-signals">Step 2: OpenTelemetry Creates Telemetry Signals</h3>
<p>Once your application is instrumented, OpenTelemetry starts producing telemetry data about what your application is doing. That data comes in three forms, called signals, which we've already briefly talked about: <a href="https://opentelemetry.io/docs/concepts/signals/traces/">traces</a>, <a href="https://opentelemetry.io/docs/concepts/signals/metrics/">metrics</a>, and <a href="https://opentelemetry.io/docs/concepts/signals/logs/">logs</a>. Each signal answers a different kind of observability question.</p>
<p><strong>Traces</strong> show you how a request moved through your system, which services it touched, and how long each step took. <strong>Metrics</strong> give you numbers over time, things like request rate, error rate, and memory usage. <strong>Logs</strong> are timestamped records of specific events that happened inside your application.</p>
<p>Here's a quick comparison:</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>What to shows</th>
<th>Example</th>
<th>Question it answers</th>
</tr>
</thead>
<tbody><tr>
<td>Trace</td>
<td>The journey of a request through your system</td>
<td>A checkout request passing through your API, order service, and database</td>
<td>Why is this request slow? Where did it fail?</td>
</tr>
<tr>
<td>Metric</td>
<td>A measured value over time</td>
<td>200 requests per second, 95ms average response time</td>
<td>Is my application healthy right now?</td>
</tr>
<tr>
<td>Log</td>
<td>A record of a specific event</td>
<td><code>ERROR: payment failed for order #1234</code></td>
<td>What exactly happened at this point in time?</td>
</tr>
</tbody></table>
<p>You don't have to choose between them. In practice, you'll use all three together. A metric tells you something is wrong, a trace shows you where, and a log tells you exactly what happened.</p>
<h3 id="heading-step-3-traces-follow-requests-through-your-application">Step 3: Traces Follow Requests Through Your Application</h3>
<p>When a user sends a request to your application, that request usually touches multiple services before a response comes back. A trace is the complete record of that journey, from the moment the request enters your system to the moment it finishes.</p>
<p>A trace is actually made up of smaller units called <strong>spans</strong>. Each span represents one operation, like an API call, a database query, or a function execution, and together they give you the full picture of what happened.</p>
<p>Every trace gets a unique trace ID, and every span gets its own span ID. The trace ID is what links all the spans together. No matter how many services a request passes through, they all share the same trace ID, so you can follow the request from start to finish in your observability backend.</p>
<p>Here's a simple example. A user places an order, and the request flows through four services:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/f1672972-b0d7-4a60-964a-7583946affad.png" alt="Trace tree diagram showing four spans under trace ID abc123: API Gateway (0–5ms), Order Service (5–20ms), Payment Service (20–45ms), and Database (45–50ms)." style="display: block;" width="1593" height="760" loading="lazy">

<p>Each span has a start time and an end time, so you can see how long each operation took. If something slowed down or failed, you can pinpoint exactly where it happened just by looking at the spans.</p>
<h3 id="heading-step-4-context-propagation-connects-work-across-services">Step 4: Context Propagation Connects Work Across Services</h3>
<p>In Step 3, you saw how a single trace is made up of spans from multiple services. But here's a question you need to ask: how does OpenTelemetry know that a span in your payment service belongs to the same trace as a span in your order service?</p>
<p>Without something connecting them, each service would record its own spans independently. Your API gateway would see one operation, your order service would see another, and your payment service would see a third. They'd look like completely separate requests with no relationship to each other, which makes debugging across services nearly impossible.</p>
<p>That's where context propagation comes in. As a request moves from one service to another, OpenTelemetry attaches the trace context to it, typically as HTTP headers. That context carries the trace ID and the parent span ID, so every service that handles the request knows which trace it belongs to and where it sits in the chain.</p>
<p>Here's what that looks like in practice:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/526efe4d-2d79-4cdb-b42d-80d8b304312a.png" alt="Context propagation diagram showing trace ID abc123 traveling across API Gateway (span-id: 001), Order Service (span-id: 002), and Payment Service (span-id: 003) via HTTP headers." style="display: block;" width="1600" height="700" loading="lazy">

<p>All three services share the same trace ID. That's what lets your observability backend connect the spans together into one complete trace.</p>
<p>OpenTelemetry doesn't invent its own rules for this. It follows the <a href="https://opentelemetry.io/docs/concepts/context-propagation/#propagation">W3C Trace Context</a> standard, a widely adopted specification that defines how trace context should be formatted and passed between services, so it works consistently across different languages, frameworks, and vendors.</p>
<p>The good news is that if you're using automatic instrumentation, context propagation happens automatically. OpenTelemetry handles the headers for you, so you don't have to think about it unless you're working with a custom transport or a non-standard setup.</p>
<h3 id="heading-step-5-the-opentelemetry-sdk-processes-the-telemetry">Step 5: The OpenTelemetry SDK Processes the Telemetry</h3>
<p>At this point, OpenTelemetry is capturing telemetry and keeping traces connected across services. But between the moment a span is created and the moment it leaves your application, something has to process it. That's the SDK's job.</p>
<p>When your instrumented code creates a span, it does that through the OpenTelemetry API. The API is what you interact with as a developer, things like <code>trace.getTracer()</code> and <code>tracer.startActiveSpan()</code>. But the API alone doesn't process or send anything. It needs the SDK behind it to actually do the work.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/e35443fa-2eff-461d-983a-d6bf3bcd5163.png" alt="A flow diagram showing what happens inside OpenTelemetry before data leaves your application: instrumentation creates telemetry, the API receives it, the SDK processes it, a processor prepares it, and the exporter sends it out." style="display: block;" width="1600" height="400" loading="lazy">

<p>Once the SDK receives the telemetry, it runs it through a processor. The processor is responsible for things like batching spans together before sending them, adding extra attributes, or filtering out data you don't need. The most common one you'll see is the <code>BatchSpanProcessor</code>, which groups spans and exports them in batches rather than one at a time, making it more efficient in production.</p>
<p>Before the processor even runs, the SDK also handles sampling. Sampling lets you control how much telemetry you actually collect. In high-traffic applications, recording every single span would generate an enormous amount of data. With sampling, you can tell the SDK to only capture a percentage of traces, which keeps your costs and data volume manageable without losing visibility.</p>
<p>Once the processor is done, it hands the data to the exporter, which is what actually sends it to its destination. You'll see how that works in the next step.</p>
<h3 id="heading-step-6-exporters-send-the-telemetry">Step 6: Exporters Send the Telemetry</h3>
<p>The exporter's job is simple: take the telemetry the SDK prepared and send it to whatever destination you've configured.</p>
<p>OpenTelemetry uses OTLP, the OpenTelemetry Protocol, to transport telemetry data. It's a standard wire protocol designed specifically for transmitting traces, metrics, and logs, and it runs over either HTTP or gRPC.</p>
<p>[!NOTE] OTLP and OpenTelemetry are not the same thing. OpenTelemetry is the full framework, covering instrumentation, the SDK, the Collector, and more. OTLP is just the protocol it uses to transport data.</p>
<p>Although OTLP is the default, not every exporter uses it. Some exporters send data directly to specific backends in their own format, like Jaeger or Prometheus. So depending on your setup, you might use an OTLP exporter to send data to a Collector or backend, or a vendor-specific exporter to send it directly."</p>
<h3 id="heading-step-7-the-opentelemetry-collector-receives-and-processes-the-data">Step 7: The OpenTelemetry Collector Receives and Processes the Data</h3>
<p>The OpenTelemetry Collector is a standalone service that sits between your application and your observability backend. It receives telemetry data, processes it, and forwards it to one or more destinations.</p>
<p>Using the Collector is common but not mandatory. You can configure your exporter to send data directly to your backend and skip the Collector entirely. But in most production setups, teams add a Collector because it gives them a central place to manage telemetry, without touching application code.</p>
<p>The Collector has three stages:</p>
<ul>
<li><p><strong>Receivers</strong> accept incoming telemetry from your applications, typically over OTLP.</p>
</li>
<li><p><strong>Processors</strong> transform the data at the Collector level, things like batching spans, filtering out noise, or adding attributes before forwarding.</p>
</li>
<li><p><strong>Exporters</strong> send the processed data to your backend, or multiple backends if needed.</p>
</li>
</ul>
<p>Here's a minimal Collector configuration:</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:

exporters:
  otlphttp:
    endpoint: https://your-backend.com

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
</code></pre>
<p>This config accepts traces over gRPC, batches them for efficiency, and forwards them to an observability backend over HTTP.</p>
<p>The Collector can also receive data from multiple applications simultaneously and route it to one or more destinations. So instead of each application shipping telemetry directly to your backend, they all send it to the Collector, and the Collector handles the rest.</p>
<p>For the full configuration, see the <a href="https://opentelemetry.io/docs/collector/configuration/">OpenTelemetry Collector configuration docs</a>.</p>
<h3 id="heading-step-8-an-observability-backend-stores-and-analyzes-the-telemetry">Step 8: An Observability Backend Stores and Analyzes the Telemetry</h3>
<p>This is where OpenTelemetry's job ends. Once your telemetry leaves the Collector, it arrives at your observability backend.</p>
<p>The backend is what stores your data, lets you query it, and gives you the dashboards and alerts you actually interact with day to day. OpenTelemetry doesn't provide any of that. It gets the data there, and the backend does the rest.</p>
<p>A few popular backends that support OpenTelemetry natively:</p>
<ul>
<li><p><strong>Open-source:</strong> Jaeger, Prometheus, Grafana Tempo</p>
</li>
<li><p><strong>Commercial:</strong> Datadog, New Relic, Honeycomb, Dynatrace, Elastic, Lightstep, Grafana Cloud, Middleware.</p>
</li>
</ul>
<p>Once your data is in the backend, you can search through traces to debug a slow request, build dashboards to monitor your application's health, and set up alerts when something goes wrong.</p>
<h2 id="heading-putting-the-opentelemetry-flow-together">Putting the OpenTelemetry Flow Together</h2>
<p>Let’s assume a user initiates a bank transfer on a mobile banking app. The request hits your API gateway, and because your application is instrumented, OpenTelemetry immediately starts capturing what’s happening. It creates a span for the incoming request and assigns it a trace ID.</p>
<p>As the request moves to the authentication service, context propagation carries that trace ID along in the request headers. The authentication service creates its own span and attaches it to the same trace. The same thing happens when the authentication service calls the transaction service, and when the transaction service hits the database to process the transfer. Four services and four spans, with one trace ID connecting them all.</p>
<p>Meanwhile, the SDK processes the telemetry in the background, runs the spans through the batch processor, and hands them to the exporter. The exporter packages everything into OTLP and sends it to the Collector, which applies your processing rules and forwards it to your observability backend.</p>
<p>Here’s a summary of every component involved in that flow:</p>
<table>
<thead>
<tr>
<th><strong>Component</strong></th>
<th>Role</th>
</tr>
</thead>
<tbody><tr>
<td>Instrumentation</td>
<td>Captures what’s happening inside your application</td>
</tr>
<tr>
<td>API</td>
<td>Exposes the methods your code calls to create spans, metrics, and logs</td>
</tr>
<tr>
<td>SDK</td>
<td>Processes and prepares telemetry for export</td>
</tr>
<tr>
<td>Exporter</td>
<td>Packages and sends telemetry via OTLP</td>
</tr>
<tr>
<td>Collector</td>
<td>Receives, processes, and routes telemetry to your backend</td>
</tr>
<tr>
<td>Observability backend</td>
<td>Stores, queries, and visualizes your telemetry</td>
</tr>
</tbody></table>
<p>From that single transfer request, you now have a complete trace in your backend. When you open your dashboard and search the trace ID, you'll see every service, every span, and every millisecond of that transaction laid out in front of you.</p>
<h2 id="heading-do-you-need-every-opentelemetry-component">Do You Need Every OpenTelemetry Component?</h2>
<p>To be honest, you don’t need every component to get started with OpenTelemetry. The pipeline you’ve seen throughout this article is the full setup, but not every team uses all of that.</p>
<ol>
<li><p><strong>Without the Collector:</strong> Your exporter sends telemetry directly to your backend with nothing in between. Works well for smaller projects or when you're just getting started.</p>
</li>
<li><p><strong>With the Collector:</strong> The more common production setup. Teams add the Collector when they need more control, like routing telemetry to multiple backends, filtering sensitive data, or managing telemetry from dozens of services in one place.</p>
</li>
</ol>
<p>The Collector is powerful, but it’s not mandatory. Start without it if your setup is simple, and add it when you actually need it.</p>
<h2 id="heading-why-use-opentelemetry">Why Use OpenTelemetry?</h2>
<p>You've now seen how every piece of OpenTelemetry fits together. Here's why it's worth using:</p>
<ul>
<li><p><strong>Vendor-neutral instrumentation:</strong> Before OpenTelemetry, switching observability tools meant you had to re-instrument your entire application from scratch. With OpenTelemetry, you instrument once and switch backends freely.</p>
</li>
<li><p><strong>Consistent telemetry across services and languages:</strong> Your backend might be in Go, your microservices in Python, and your data pipeline in Java. OpenTelemetry has SDKs for all of them, and they all produce telemetry in the same format, so your whole stack speaks the same language.</p>
</li>
<li><p><strong>Correlated observability signals:</strong> Traces, metrics, and logs all flow through the same pipeline. So when something goes wrong, a spike in your metrics leads you to a trace, and that trace points you to the exact log entry where things broke down.</p>
</li>
<li><p><strong>It's becoming the industry standard:</strong> Most observability backends already support OpenTelemetry natively. Instead of learning a vendor-specific instrumentation approach every time you adopt a new tool, you learn OpenTelemetry once and it works everywhere.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>OpenTelemetry gives you a standard way to instrument your application, collect telemetry, and ship it to any backend you choose, without locking you into a specific vendor or tool.</p>
<p>If you remember nothing else from this article, remember this: you instrument your application, generate telemetry, process it, export it, and analyze it. Every step has a component behind it, and now you know what each one does.</p>
<p>If you're just getting started, you don't need to set everything up at once. Start with instrumentation, get your telemetry flowing to a backend, and add the Collector when you need it. You can always add more as your needs grow.</p>
<p>If you found this helpful, I'd love to connect. You can find me on <a href="https://www.linkedin.com/in/chosenvincent1">LinkedIn</a> or <a href="https://x.com/ChosenVincent1">X</a>. Feel free to reach out if you have questions or just want to talk about observability and developer tooling.</p>
 ]]>
                </content:encoded>
            </item>
        
            <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;" width="600" height="400" 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;" width="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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>
