Instrument a Landing Page with GA4

Set up GA4 custom event tracking and write a data-backed product recommendation.

Introduction

30 Second Summary

Every product manager has been in a meeting where someone says "I think users want this feature." The PMs who stand out are the ones who can point to real data instead.

In this project, you will instrument a fictional SaaS landing page with Google Analytics 4 custom events. You will then turn that data into a product recommendation for your PM portfolio.

What You'll Build

You will build a portfolio-ready analytics case study that starts with a fully instrumented product landing page and ends with a written recommendation backed by real data.

By the end of this project, you'll have:

  • A GA4 property with a web data stream collecting custom events from a product landing page.
  • Two GA4 Explorations (a free-form event report and a funnel visualization) that you can screenshot for your portfolio.
  • A written data-backed product recommendation document that demonstrates your product thinking to hiring managers.
  • Secret Mission: Add a device-category comparison segment to your funnel and include a mobile vs. desktop insight in your recommendation.

Are there any prerequisites?

You need a Google account to access GA4. Everything else (VS Code, the Live Server extension) is free and covered in Step 1.

Set Up Your Tools and GA4 Property

Before you can track anything on your landing page, you need two things: a code editor to build the page and an analytics property to collect the data.

In this step, you will install VS Code with a local server extension so GA4 can track your page over HTTP. Then you will create a GA4 property with a web data stream that is ready to receive events.

In this step, get ready to:
  • Install VS Code and the Live Server extension.
  • Create a GA4 account, property, and web data stream.
  • Copy your Measurement ID and Google tag snippet.
Install VS Code and Live Server

GA4 does not reliably track pages opened directly from your file system using the file:// protocol. You need a local web server that serves your page over HTTP so GA4 can collect data from it.

What is Live Server?

Live Server is a VS Code extension that runs a small web server on your computer. It serves your HTML files at a local address like http://127.0.0.1:5500 and auto-refreshes your browser whenever you save a file.

You may already have VS Code installed on your computer.

✔️ I already have VS Code

Skip ahead to installing the Live Server extension below.

ⓧ I need to install it

  • Open your browser and go to https://code.visualstudio.com/.
  • Click the download button for your operating system.
  • Run the installer and follow the default prompts to complete the installation.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Live Server in the search bar.
  • Find the extension by Ritwick Dey and click Install.

You should see a green "Installed" badge on the Live Server extension card after a few seconds.

Don't see Live Server in the results?

Make sure you are typing in the Extensions search bar at the top of the panel, not the main VS Code search bar. The extension author is Ritwick Dey. Look for that name to confirm you have the correct one.

Help me find and install the Live Server extension in VS Code.

Create a GA4 account, property, and data stream

GA4 organizes your analytics into three layers. An account is the top-level container (one per person or organization). A property sits inside an account and holds data for one product or website. A data stream tells the property where to collect data from.

You will create all three now in a single flow.

  • Open your browser and go to https://analytics.google.com/.
  • Sign in with your Google account if prompted.
  • Click Admin in the left sidebar (the gear icon at the bottom).
  • Click Create, then select Account.
  • Enter Portfolio Projects as the account name.
  • Leave the data sharing settings at their defaults and click Next.

You are now on the property creation screen.

  • Enter TaskFlow Landing Page as the property name.
  • Set the Reporting time zone to your local time zone.
  • Set the Currency to your local currency.
  • Click Next.
  • GA4 asks how you intend to use Analytics. Select Examine user behavior.
  • Click Create.
  • Accept the Google Analytics terms of service if prompted.

What is a GA4 property?

A property is where GA4 stores and processes all the analytics data for one product or website. Think of the account as a folder and the property as the project inside it.

A single account can hold up to 2,000 properties. You only need one for this project.

GA4 now asks you to choose a platform for data collection. You will create a web data stream so GA4 knows to listen for events from a website.

  • Click Web.
  • Enter localhost as the Website URL.
  • Enter TaskFlow Local Dev as the Stream name.
  • Make sure the Enhanced measurement toggle is switched on.
  • Click Create stream.

GA4 creates your data stream and opens the stream details page. You should see a Measurement ID at the top of the page starting with G-.

What is Enhanced Measurement?

Enhanced Measurement automatically tracks common interactions like page views, scrolls, outbound clicks, and file downloads. You get useful baseline data without writing any extra code.

Don't see the data stream details?

If you navigated away from the stream details page, go to Admin in the left sidebar. Under Data collection and modification, click Data Streams. Click your TaskFlow Local Dev stream to open its details.

Help me find my data stream details page in GA4.

Copy your Measurement ID and Google tag snippet

Your Measurement ID is the unique identifier that links your HTML page to this specific GA4 data stream. The Google tag snippet is a small block of JavaScript that you will paste into your page's code to start sending data.

  • On the data stream details page, find your Measurement ID (it starts with G-).
  • Click the copy icon next to the Measurement ID to copy it.
  • Paste it somewhere safe (a text file or sticky note). You will need it in the next step.

Now grab the full Google tag snippet that contains the gtag.js script.

  • On the same data stream details page, click View tag instructions.
  • Select the Install manually tab.
  • Copy the entire code snippet shown on the page.
  • Paste the snippet into the same safe location as your Measurement ID.

What does the Google tag snippet do?

The snippet loads the gtag.js library from Google's servers and initializes it with your Measurement ID. Once it runs on your page, it automatically sends a page_view event to GA4 every time someone opens your page.

You will also add custom events on top of this baseline in a later step.

Keep this tab open

Leave your GA4 browser tab open. You will switch between your code editor and the GA4 console frequently throughout this project.

Your tools are installed and your GA4 property is live. Next up, you will build the TaskFlow landing page and wire up the Google tag so GA4 starts collecting data.

Build the TaskFlow Landing Page

Your GA4 property is set up, but it has nothing to track yet. Analytics only becomes useful when there is a real page sending data back to it.

In this step, you will build a landing page for a fictional SaaS product called TaskFlow. You will paste in the Google tag snippet so GA4 starts collecting page views immediately.

In this step, get ready to:
  • Create the index.html landing page file in VS Code.
  • Launch the page locally with Live Server.
  • Verify that GA4 is receiving a page_view event.
Create the index.html file

The landing page has four sections that map to key moments in a user journey. Each section will become a tracking point for GA4 custom events in the next step.

  • Press Cmd+N (macOS) or Ctrl+N (Windows) to create a new file in VS Code.
  • Copy all of the following landing page code and paste it into the new file:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TaskFlow - Project Management Made Simple</title>

  <!-- Google tag (gtag.js) -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=[[GA4_ID="your Measurement ID (starts with G-)"]]"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', '[[GA4_ID="your Measurement ID (starts with G-)"]]', { 'debug_mode': true });
  </script>

  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a2e; line-height: 1.6; }

    /* Hero */
    .hero {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white; text-align: center; padding: 80px 20px;
    }
    .hero h1 { font-size: 2.5rem; margin-bottom: 16px; }
    .hero p { font-size: 1.2rem; margin-bottom: 32px; opacity: 0.9; max-width: 600px; margin-left: auto; margin-right: auto; }
    .cta-button {
      background: #fff; color: #667eea; border: none; padding: 16px 40px;
      font-size: 1.1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .cta-button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.2); }

    /* Features */
    .features { padding: 60px 20px; text-align: center; background: #f8f9fa; }
    .features h2 { font-size: 2rem; margin-bottom: 40px; }
    .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; max-width: 900px; margin: 0 auto; }
    .feature-card { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
    .feature-card h3 { margin-bottom: 10px; color: #667eea; }

    /* Pricing */
    .pricing { padding: 60px 20px; text-align: center; }
    .pricing h2 { font-size: 2rem; margin-bottom: 40px; }
    .pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; max-width: 700px; margin: 0 auto; }
    .pricing-card {
      border: 2px solid #e0e0e0; border-radius: 12px; padding: 40px 30px;
      transition: border-color 0.2s, transform 0.2s;
    }
    .pricing-card:hover { border-color: #667eea; transform: translateY(-4px); }
    .pricing-card h3 { font-size: 1.4rem; margin-bottom: 8px; }
    .pricing-card .price { font-size: 2.5rem; font-weight: 700; color: #667eea; margin-bottom: 16px; }
    .pricing-card .price span { font-size: 1rem; font-weight: 400; color: #666; }
    .pricing-card ul { list-style: none; margin-bottom: 24px; text-align: left; }
    .pricing-card ul li { padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
    .pricing-card ul li::before { content: "✓ "; color: #667eea; font-weight: 600; }
    .plan-button {
      background: #667eea; color: white; border: none; padding: 12px 32px;
      font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
      transition: background 0.2s;
    }
    .plan-button:hover { background: #764ba2; }

    /* Newsletter */
    .newsletter { padding: 60px 20px; text-align: center; background: #f8f9fa; }
    .newsletter h2 { font-size: 1.8rem; margin-bottom: 16px; }
    .newsletter p { margin-bottom: 24px; color: #666; }
    .newsletter-form { display: flex; gap: 12px; max-width: 450px; margin: 0 auto; flex-wrap: wrap; justify-content: center; }
    .newsletter-form input {
      flex: 1; min-width: 200px; padding: 12px 16px; border: 2px solid #e0e0e0;
      border-radius: 8px; font-size: 1rem;
    }
    .newsletter-form input:focus { outline: none; border-color: #667eea; }
    .newsletter-form button {
      background: #667eea; color: white; border: none; padding: 12px 24px;
      font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
    }

    /* Footer */
    .footer { padding: 30px 20px; text-align: center; color: #999; font-size: 0.9rem; }
  </style>
</head>
<body>

  <!-- Hero Section -->
  <section class="hero">
    <h1>Manage Projects Without the Chaos</h1>
    <p>TaskFlow brings your team's tasks, timelines, and communication into one simple tool. No more juggling five apps.</p>
    <button class="cta-button" id="hero-cta">Start Free Trial</button>
  </section>

  <!-- Features Section -->
  <section class="features">
    <h2>Everything Your Team Needs</h2>
    <div class="features-grid">
      <div class="feature-card">
        <h3>Task Boards</h3>
        <p>Drag-and-drop Kanban boards that keep everyone aligned on what needs to happen next.</p>
      </div>
      <div class="feature-card">
        <h3>Timeline View</h3>
        <p>See project milestones and deadlines at a glance with automatic Gantt charts.</p>
      </div>
      <div class="feature-card">
        <h3>Team Chat</h3>
        <p>Built-in messaging so conversations stay connected to the work they are about.</p>
      </div>
    </div>
  </section>

  <!-- Pricing Section -->
  <section class="pricing" id="pricing">
    <h2>Simple, Transparent Pricing</h2>
    <div class="pricing-grid">
      <div class="pricing-card">
        <h3>Starter</h3>
        <div class="price">$9<span>/user/mo</span></div>
        <ul>
          <li>Up to 10 team members</li>
          <li>Unlimited task boards</li>
          <li>Basic timeline view</li>
          <li>Email support</li>
        </ul>
        <button class="plan-button" data-plan="starter">Choose Starter</button>
      </div>
      <div class="pricing-card">
        <h3>Pro</h3>
        <div class="price">$19<span>/user/mo</span></div>
        <ul>
          <li>Unlimited team members</li>
          <li>Advanced timeline and Gantt</li>
          <li>Team chat included</li>
          <li>Priority support</li>
        </ul>
        <button class="plan-button" data-plan="pro">Choose Pro</button>
      </div>
    </div>
  </section>

  <!-- Newsletter Section -->
  <section class="newsletter">
    <h2>Get Product Updates</h2>
    <p>Join 2,000+ product managers getting weekly tips on shipping better products.</p>
    <form class="newsletter-form" id="newsletter-form">
      <input type="email" id="newsletter-email" placeholder="you@company.com" required>
      <button type="submit">Subscribe</button>
    </form>
  </section>

  <!-- Footer -->
  <footer class="footer">
    <p>TaskFlow is a fictional product built for a GA4 analytics portfolio project.</p>
  </footer>

</body>
</html>

What does this code do?

The Google tag snippet in the <head> loads the gtag.js tracking library and connects it to your GA4 property using your Measurement ID.

The debug_mode: true setting in the config call tells GA4 to send events to DebugView in real time. Without it, you would have to wait up to 48 hours for data to appear in standard reports.

The page itself has four sections: a hero with a "Start Free Trial" button, a features overview, pricing plans (Starter at $9 and Pro at $19), and a newsletter signup form. Each section has specific IDs and classes like id="hero-cta" and data-plan="starter" that you will target with custom event tracking in the next step.

Why gtag.js instead of Google Tag Manager?

For a single-page portfolio project, gtag.js is the simpler choice. Google Tag Manager adds an extra layer of configuration (containers, tags, triggers) that does not help you learn the core analytics skill here.

With gtag.js, you see the direct relationship between your code and the events GA4 receives. That builds a stronger mental model of how product analytics works under the hood.

  • Press Cmd+S (macOS) or Ctrl+S (Windows) to open the save dialog.
  • Navigate to your Desktop folder in the save dialog.
  • Type index.html as the file name and click Save.

VS Code asking you to select a language?

If VS Code shows a language selection prompt, pick HTML. This tells VS Code to apply HTML syntax highlighting to your file.

help me fix VS Code language selection for my HTML file

✔️ Awesome, I've got everything!

Great. Double check that you have saved the file as index.html on your Desktop before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file. Compare it line by line with what you have in VS Code.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TaskFlow - Project Management Made Simple</title>

  <!-- Google tag (gtag.js) -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=[[GA4_ID="your Measurement ID (starts with G-)"]]"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', '[[GA4_ID="your Measurement ID (starts with G-)"]]', { 'debug_mode': true });
  </script>

  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a2e; line-height: 1.6; }

    /* Hero */
    .hero {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white; text-align: center; padding: 80px 20px;
    }
    .hero h1 { font-size: 2.5rem; margin-bottom: 16px; }
    .hero p { font-size: 1.2rem; margin-bottom: 32px; opacity: 0.9; max-width: 600px; margin-left: auto; margin-right: auto; }
    .cta-button {
      background: #fff; color: #667eea; border: none; padding: 16px 40px;
      font-size: 1.1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .cta-button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.2); }

    /* Features */
    .features { padding: 60px 20px; text-align: center; background: #f8f9fa; }
    .features h2 { font-size: 2rem; margin-bottom: 40px; }
    .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; max-width: 900px; margin: 0 auto; }
    .feature-card { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
    .feature-card h3 { margin-bottom: 10px; color: #667eea; }

    /* Pricing */
    .pricing { padding: 60px 20px; text-align: center; }
    .pricing h2 { font-size: 2rem; margin-bottom: 40px; }
    .pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; max-width: 700px; margin: 0 auto; }
    .pricing-card {
      border: 2px solid #e0e0e0; border-radius: 12px; padding: 40px 30px;
      transition: border-color 0.2s, transform 0.2s;
    }
    .pricing-card:hover { border-color: #667eea; transform: translateY(-4px); }
    .pricing-card h3 { font-size: 1.4rem; margin-bottom: 8px; }
    .pricing-card .price { font-size: 2.5rem; font-weight: 700; color: #667eea; margin-bottom: 16px; }
    .pricing-card .price span { font-size: 1rem; font-weight: 400; color: #666; }
    .pricing-card ul { list-style: none; margin-bottom: 24px; text-align: left; }
    .pricing-card ul li { padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
    .pricing-card ul li::before { content: "✓ "; color: #667eea; font-weight: 600; }
    .plan-button {
      background: #667eea; color: white; border: none; padding: 12px 32px;
      font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
      transition: background 0.2s;
    }
    .plan-button:hover { background: #764ba2; }

    /* Newsletter */
    .newsletter { padding: 60px 20px; text-align: center; background: #f8f9fa; }
    .newsletter h2 { font-size: 1.8rem; margin-bottom: 16px; }
    .newsletter p { margin-bottom: 24px; color: #666; }
    .newsletter-form { display: flex; gap: 12px; max-width: 450px; margin: 0 auto; flex-wrap: wrap; justify-content: center; }
    .newsletter-form input {
      flex: 1; min-width: 200px; padding: 12px 16px; border: 2px solid #e0e0e0;
      border-radius: 8px; font-size: 1rem;
    }
    .newsletter-form input:focus { outline: none; border-color: #667eea; }
    .newsletter-form button {
      background: #667eea; color: white; border: none; padding: 12px 24px;
      font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
    }

    /* Footer */
    .footer { padding: 30px 20px; text-align: center; color: #999; font-size: 0.9rem; }
  </style>
</head>
<body>

  <!-- Hero Section -->
  <section class="hero">
    <h1>Manage Projects Without the Chaos</h1>
    <p>TaskFlow brings your team's tasks, timelines, and communication into one simple tool. No more juggling five apps.</p>
    <button class="cta-button" id="hero-cta">Start Free Trial</button>
  </section>

  <!-- Features Section -->
  <section class="features">
    <h2>Everything Your Team Needs</h2>
    <div class="features-grid">
      <div class="feature-card">
        <h3>Task Boards</h3>
        <p>Drag-and-drop Kanban boards that keep everyone aligned on what needs to happen next.</p>
      </div>
      <div class="feature-card">
        <h3>Timeline View</h3>
        <p>See project milestones and deadlines at a glance with automatic Gantt charts.</p>
      </div>
      <div class="feature-card">
        <h3>Team Chat</h3>
        <p>Built-in messaging so conversations stay connected to the work they are about.</p>
      </div>
    </div>
  </section>

  <!-- Pricing Section -->
  <section class="pricing" id="pricing">
    <h2>Simple, Transparent Pricing</h2>
    <div class="pricing-grid">
      <div class="pricing-card">
        <h3>Starter</h3>
        <div class="price">$9<span>/user/mo</span></div>
        <ul>
          <li>Up to 10 team members</li>
          <li>Unlimited task boards</li>
          <li>Basic timeline view</li>
          <li>Email support</li>
        </ul>
        <button class="plan-button" data-plan="starter">Choose Starter</button>
      </div>
      <div class="pricing-card">
        <h3>Pro</h3>
        <div class="price">$19<span>/user/mo</span></div>
        <ul>
          <li>Unlimited team members</li>
          <li>Advanced timeline and Gantt</li>
          <li>Team chat included</li>
          <li>Priority support</li>
        </ul>
        <button class="plan-button" data-plan="pro">Choose Pro</button>
      </div>
    </div>
  </section>

  <!-- Newsletter Section -->
  <section class="newsletter">
    <h2>Get Product Updates</h2>
    <p>Join 2,000+ product managers getting weekly tips on shipping better products.</p>
    <form class="newsletter-form" id="newsletter-form">
      <input type="email" id="newsletter-email" placeholder="you@company.com" required>
      <button type="submit">Subscribe</button>
    </form>
  </section>

  <!-- Footer -->
  <footer class="footer">
    <p>TaskFlow is a fictional product built for a GA4 analytics portfolio project.</p>
  </footer>

</body>
</html>
Launch the page with Live Server

GA4 cannot track pages opened directly from your file system (the file:// protocol). Live Server creates a local web server so your page is served over HTTP, which GA4 can track normally.

  • In the VS Code Explorer sidebar on the left, find your index.html file.
  • Right-click index.html and select Open with Live Server.

Your browser opens to http://127.0.0.1:5500/index.html. You should see the TaskFlow landing page with a purple gradient hero, a "Start Free Trial" button, three feature cards, two pricing plans, and a newsletter form.

Don't see Open with Live Server?

Make sure you installed the Live Server extension in Step 1. Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows), search for "Live Server" by Ritwick Dey, and click Install.

After installing, right-click index.html again. The option should now appear in the context menu.

Help me get Live Server working in VS Code

Verify the page view in GA4 Realtime

The Google tag fires a page_view event automatically every time someone loads the page. You can confirm GA4 is receiving it by checking the Realtime report.

  • Go to your GA4 property at analytics.google.com.
  • In the left sidebar, click Reports.
  • Click Realtime.

You should see at least 1 active user and a page_view event listed in the "Event count by Event name" card. It can take up to 30 seconds for data to appear in a new property.

Not seeing any data in Realtime?

  • Disable any ad blockers in your browser. Ad blockers commonly block the googletagmanager.com script, which prevents GA4 from loading at all.
  • Confirm your Measurement ID in index.html matches the one shown in your GA4 data stream details. It should start with G- and appear twice in the file.
  • Make sure you are viewing the page through Live Server (http://127.0.0.1:5500), not by opening the file directly from your Desktop.
  • Refresh the landing page in your browser, then wait 30 seconds and check Realtime again.

Help me troubleshoot why GA4 Realtime is not showing my page view

Your landing page is live and GA4 is already collecting page views. Next up, you will add custom event tracking so GA4 captures exactly how users interact with each section of the page.

Instrument Custom Events

Your TaskFlow landing page is live and GA4 is already tracking page views. That confirms the connection between your page and your property is working.

Page views alone tell you that someone visited. They do not tell you what that person did on the page. In this step, you will add custom events that track four specific user interactions: clicking the CTA button, viewing the pricing section, selecting a plan, and submitting the newsletter form.

In this step, get ready to:
  • Add JavaScript event listeners that send four custom events to GA4.
  • Verify all four events in GA4 DebugView.
  • Mark plan selection as a key event to define your conversion metric.
Add custom event tracking code

Each custom event uses the gtag() function that your Google tag snippet already loaded in the <head>. You call gtag('event', 'event_name', { parameters }) whenever a user does something you want to measure.

  • In index.html, scroll to the bottom of the file and find the closing </footer> tag.
  • Add the following code below </footer> and above the closing </body> tag:
  <!-- Custom Event Tracking -->
  <script>
    // Event 1: CTA button click
    document.getElementById('hero-cta').addEventListener('click', function() {
      gtag('event', 'click_cta', {
        'button_location': 'hero'
      });
    });

    // Event 2: Pricing section comes into view
    var pricingSection = document.getElementById('pricing');
    var pricingObserver = new IntersectionObserver(function(entries) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          gtag('event', 'view_pricing');
          pricingObserver.unobserve(pricingSection);
        }
      });
    }, { threshold: 0.5 });
    pricingObserver.observe(pricingSection);

    // Event 3: Plan selection
    var planButtons = document.querySelectorAll('.plan-button');
    planButtons.forEach(function(button) {
      button.addEventListener('click', function() {
        gtag('event', 'select_plan', {
          'plan_name': this.getAttribute('data-plan')
        });
      });
    });

    // Event 4: Newsletter signup
    document.getElementById('newsletter-form').addEventListener('submit', function(e) {
      e.preventDefault();
      var email = document.getElementById('newsletter-email').value;
      var domain = email.split('@')[1] || 'unknown';
      gtag('event', 'submit_newsletter', {
        'email_domain': domain
      });
      document.getElementById('newsletter-email').value = '';
      alert('Thanks for subscribing!');
    });
  </script>

What does this code do?

  • Event 1 (click_cta): Listens for a click on the "Start Free Trial" button and sends an event with a button_location parameter set to hero. This tells you which CTA the user clicked if you add more CTAs later.
  • Event 2 (view_pricing): Uses an IntersectionObserver to detect when the pricing section scrolls into view. The threshold: 0.5 means the event fires once 50% of the section is visible. It fires only once because the observer unsubscribes itself after the first trigger.
  • Event 3 (select_plan): Listens for clicks on both pricing card buttons. The plan_name parameter reads the data-plan attribute from the button, so GA4 records whether the user chose "starter" or "pro".
  • Event 4 (submit_newsletter): Listens for the newsletter form submission. It extracts the email domain (the part after @) as a parameter, clears the input field, and shows a confirmation alert.
  • Save index.html.

You will not see a visible change on the page yet. The next substep verifies these events are firing correctly in DebugView.

Seeing an error in the browser console?

Make sure your new <script> block is placed after </footer> and before </body>. Placing it inside the <head> or before the HTML elements it references will cause errors because those elements do not exist yet when the script runs.

Also check that you did not accidentally paste the code inside the existing <style> block or inside the <footer> section.

Need more help? Help me debug a browser console error after adding my custom event script to index.html.

✔️ Awesome, I've got everything!

Double check you have saved index.html before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file with the custom event script included. Compare your file against this to spot any differences.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TaskFlow - Project Management Made Simple</title>

  <!-- Google tag (gtag.js) -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-XXXXXXXXXX', { 'debug_mode': true });
  </script>

  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a2e; line-height: 1.6; }

    /* Hero */
    .hero {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white; text-align: center; padding: 80px 20px;
    }
    .hero h1 { font-size: 2.5rem; margin-bottom: 16px; }
    .hero p { font-size: 1.2rem; margin-bottom: 32px; opacity: 0.9; max-width: 600px; margin-left: auto; margin-right: auto; }
    .cta-button {
      background: #fff; color: #667eea; border: none; padding: 16px 40px;
      font-size: 1.1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .cta-button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.2); }

    /* Features */
    .features { padding: 60px 20px; text-align: center; background: #f8f9fa; }
    .features h2 { font-size: 2rem; margin-bottom: 40px; }
    .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; max-width: 900px; margin: 0 auto; }
    .feature-card { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
    .feature-card h3 { margin-bottom: 10px; color: #667eea; }

    /* Pricing */
    .pricing { padding: 60px 20px; text-align: center; }
    .pricing h2 { font-size: 2rem; margin-bottom: 40px; }
    .pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; max-width: 700px; margin: 0 auto; }
    .pricing-card {
      border: 2px solid #e0e0e0; border-radius: 12px; padding: 40px 30px;
      transition: border-color 0.2s, transform 0.2s;
    }
    .pricing-card:hover { border-color: #667eea; transform: translateY(-4px); }
    .pricing-card h3 { font-size: 1.4rem; margin-bottom: 8px; }
    .pricing-card .price { font-size: 2.5rem; font-weight: 700; color: #667eea; margin-bottom: 16px; }
    .pricing-card .price span { font-size: 1rem; font-weight: 400; color: #666; }
    .pricing-card ul { list-style: none; margin-bottom: 24px; text-align: left; }
    .pricing-card ul li { padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
    .pricing-card ul li::before { content: "✓ "; color: #667eea; font-weight: 600; }
    .plan-button {
      background: #667eea; color: white; border: none; padding: 12px 32px;
      font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
      transition: background 0.2s;
    }
    .plan-button:hover { background: #764ba2; }

    /* Newsletter */
    .newsletter { padding: 60px 20px; text-align: center; background: #f8f9fa; }
    .newsletter h2 { font-size: 1.8rem; margin-bottom: 16px; }
    .newsletter p { margin-bottom: 24px; color: #666; }
    .newsletter-form { display: flex; gap: 12px; max-width: 450px; margin: 0 auto; flex-wrap: wrap; justify-content: center; }
    .newsletter-form input {
      flex: 1; min-width: 200px; padding: 12px 16px; border: 2px solid #e0e0e0;
      border-radius: 8px; font-size: 1rem;
    }
    .newsletter-form input:focus { outline: none; border-color: #667eea; }
    .newsletter-form button {
      background: #667eea; color: white; border: none; padding: 12px 24px;
      font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer;
    }

    /* Footer */
    .footer { padding: 30px 20px; text-align: center; color: #999; font-size: 0.9rem; }
  </style>
</head>
<body>

  <!-- Hero Section -->
  <section class="hero">
    <h1>Manage Projects Without the Chaos</h1>
    <p>TaskFlow brings your team's tasks, timelines, and communication into one simple tool. No more juggling five apps.</p>
    <button class="cta-button" id="hero-cta">Start Free Trial</button>
  </section>

  <!-- Features Section -->
  <section class="features">
    <h2>Everything Your Team Needs</h2>
    <div class="features-grid">
      <div class="feature-card">
        <h3>Task Boards</h3>
        <p>Drag-and-drop Kanban boards that keep everyone aligned on what needs to happen next.</p>
      </div>
      <div class="feature-card">
        <h3>Timeline View</h3>
        <p>See project milestones and deadlines at a glance with automatic Gantt charts.</p>
      </div>
      <div class="feature-card">
        <h3>Team Chat</h3>
        <p>Built-in messaging so conversations stay connected to the work they are about.</p>
      </div>
    </div>
  </section>

  <!-- Pricing Section -->
  <section class="pricing" id="pricing">
    <h2>Simple, Transparent Pricing</h2>
    <div class="pricing-grid">
      <div class="pricing-card">
        <h3>Starter</h3>
        <div class="price">$9<span>/user/mo</span></div>
        <ul>
          <li>Up to 10 team members</li>
          <li>Unlimited task boards</li>
          <li>Basic timeline view</li>
          <li>Email support</li>
        </ul>
        <button class="plan-button" data-plan="starter">Choose Starter</button>
      </div>
      <div class="pricing-card">
        <h3>Pro</h3>
        <div class="price">$19<span>/user/mo</span></div>
        <ul>
          <li>Unlimited team members</li>
          <li>Advanced timeline and Gantt</li>
          <li>Team chat included</li>
          <li>Priority support</li>
        </ul>
        <button class="plan-button" data-plan="pro">Choose Pro</button>
      </div>
    </div>
  </section>

  <!-- Newsletter Section -->
  <section class="newsletter">
    <h2>Get Product Updates</h2>
    <p>Join 2,000+ product managers getting weekly tips on shipping better products.</p>
    <form class="newsletter-form" id="newsletter-form">
      <input type="email" id="newsletter-email" placeholder="you@company.com" required>
      <button type="submit">Subscribe</button>
    </form>
  </section>

  <!-- Footer -->
  <footer class="footer">
    <p>TaskFlow is a fictional product built for a GA4 analytics portfolio project.</p>
  </footer>

  <!-- Custom Event Tracking -->
  <script>
    // Event 1: CTA button click
    document.getElementById('hero-cta').addEventListener('click', function() {
      gtag('event', 'click_cta', {
        'button_location': 'hero'
      });
    });

    // Event 2: Pricing section comes into view
    var pricingSection = document.getElementById('pricing');
    var pricingObserver = new IntersectionObserver(function(entries) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          gtag('event', 'view_pricing');
          pricingObserver.unobserve(pricingSection);
        }
      });
    }, { threshold: 0.5 });
    pricingObserver.observe(pricingSection);

    // Event 3: Plan selection
    var planButtons = document.querySelectorAll('.plan-button');
    planButtons.forEach(function(button) {
      button.addEventListener('click', function() {
        gtag('event', 'select_plan', {
          'plan_name': this.getAttribute('data-plan')
        });
      });
    });

    // Event 4: Newsletter signup
    document.getElementById('newsletter-form').addEventListener('submit', function(e) {
      e.preventDefault();
      var email = document.getElementById('newsletter-email').value;
      var domain = email.split('@')[1] || 'unknown';
      gtag('event', 'submit_newsletter', {
        'email_domain': domain
      });
      document.getElementById('newsletter-email').value = '';
      alert('Thanks for subscribing!');
    });
  </script>

</body>
</html>
Test your events in DebugView

DebugView shows events from your browser in real time, one by one, as they fire. Because you added debug_mode: true to your Google tag config in the previous step, your page is already sending debug data to GA4.

  • In your GA4 property, click Admin in the left sidebar (the gear icon at the bottom).
  • Under Data display, click DebugView.

You should see a timeline with a seconds stream on the right side of the screen. Now switch to your TaskFlow landing page in the browser and trigger each event one at a time.

  • Click the Start Free Trial button at the top of the page.
  • Scroll down slowly until the pricing section is visible.
  • Click the Choose Starter or Choose Pro button on one of the pricing cards.
  • Type test@example.com into the newsletter email field and click Subscribe. You will see a browser alert saying "Thanks for subscribing!" confirming the form handler is working.
  • Switch back to the DebugView tab in GA4.

In the seconds stream, you should see click_cta, view_pricing, select_plan, and submit_newsletter appear as individual event bubbles. Click any event bubble to inspect its parameters.

What should I see in DebugView?

Each event appears as a blue bubble in the seconds stream on the right side. When you click a bubble, a panel opens showing the event name and its parameters.

For example, clicking the select_plan bubble should reveal a plan_name parameter with the value starter or pro, depending on which button you clicked.

Not seeing your events in DebugView?

  • Disable any ad blockers in your browser. Ad blockers commonly block the googletagmanager.com script, which prevents all events from sending.
  • Confirm that your page is being served by Live Server at http://127.0.0.1:5500. GA4 does not reliably track pages opened with the file:// protocol.
  • Make sure debug_mode: true is in your gtag('config', ...) call in the <head>. Without it, events go to standard reports only and skip DebugView entirely.
  • Wait up to 30 seconds and refresh DebugView. There can be a short delay before events appear.

Still stuck? Help me troubleshoot why my custom events are not appearing in GA4 DebugView.

Mark select_plan as a key event

GA4 treats all events equally by default. A key event tells GA4 that this particular action is the most important thing a user can do on your page. As a PM, this is how you define your conversion metric.

  • In GA4, click Admin in the left sidebar.
  • Under Data display, click Events.
  • Find select_plan in the events list.
  • Toggle the Mark as key event switch next to it.

The toggle turns blue to confirm select_plan is now a key event.

Why mark select_plan as the key event?

Choosing a pricing plan is the moment where a visitor signals purchase intent. That makes it the strongest conversion signal on this page.

Key events appear in GA4 overview reports and Explorations as a dedicated metric. When you build your funnel in the next step, this is the end goal you are optimizing toward.

Don't see select_plan in the events list?

Custom events can take up to 30 minutes to appear in the Events list. If select_plan is not listed yet, go back to your landing page and click a plan button a few more times. Then wait a few minutes and refresh the Events page.

As long as you confirmed the event in DebugView earlier, the data is being collected. The Events list just takes longer to populate.

Need help? Help me find my select_plan event in GA4 so I can mark it as a key event.

Your landing page is now fully instrumented with four custom events and a defined conversion metric. Next up, you will build two Explorations in GA4 that visualize this data for your portfolio.

Build Explorations for Your Portfolio

Your custom events are firing, verified in DebugView, and select_plan is marked as a key event. GA4 is collecting real interaction data from your TaskFlow landing page.

Raw event data on its own is hard to read. Explorations are GA4's custom reporting tool that lets you build visualizations tailored to the exact questions you want to answer. In this step, you will build two Explorations that are ready to screenshot for your PM portfolio.

In this step, get ready to:
  • Create a free-form Exploration showing event counts as a bar chart.
  • Create a funnel Exploration visualizing the user journey from page view to plan selection.
  • Screenshot both Explorations as portfolio artifacts.

Generate fresh data before you start

Explorations can take 15 to 30 minutes to populate with event data. Before starting this step, go to your TaskFlow landing page and run through the full user journey a few times: click the CTA, scroll to pricing, click a plan, and submit the newsletter form.

If you completed Step 3 more than 30 minutes ago, your data should already be available. If you just finished, generate 5 to 10 rounds of clicks and then wait a few minutes before building your Explorations.

Create a free-form Exploration

A free-form Exploration lets you pick exactly which dimensions and metrics to display. You will build one that shows how many times each event fired and how many unique users triggered it.

  • In your GA4 property, click Explore in the left sidebar.
  • Click Start a new exploration.
  • Select the Free form template.

You should see a blank exploration with two panels. The Variables panel on the left is where you choose what data to work with. The Tab Settings panel in the middle is where you arrange that data into rows, columns, and values.

  • In the Variables panel, click the + icon next to Dimensions.
  • Search for Event name, check the box next to it, and click Import.
  • Click the + icon next to Metrics.
  • Search for and select Event count.
  • Also search for and select Total users.
  • Click Import to bring both metrics in.

Now arrange the data in the Tab Settings panel.

  • Drag Event name from the Variables panel into the Rows area in Tab Settings.
  • Drag Event count into the Values area.
  • Drag Total users into the Values area as well.

A table appears on the right side of the screen showing each event name alongside its count and unique user total. This is useful, but a bar chart is more visual for a portfolio.

  • In the Tab Settings panel, find the Visualization dropdown (it currently says Table).
  • Change it to Bar chart.

You should see a bar chart showing your custom events alongside GA4's automatically collected events. Each bar represents an event, with the length showing the event count.

What does this chart tell you?

The bar chart gives you a quick visual comparison of which events fire the most. As a PM, you would use this to spot which parts of the page get the most engagement.

For example, if view_pricing fires far less than click_cta, that gap tells you users are interested enough to click the CTA but are dropping off before they ever see pricing.

Don't see your custom events in the chart?

Explorations can take 15 to 30 minutes to process newly collected data. If your chart only shows automatically collected events like page_view and session_start, go back to your TaskFlow page, generate more clicks, and check again in a few minutes.

Also confirm that your ad blocker is disabled in the browser where you are interacting with the landing page.

Help me troubleshoot why my custom events aren't showing in GA4 Explorations

Create a funnel Exploration

A bar chart shows totals, but it does not show the journey between events. A funnel visualization maps out a specific sequence of steps and shows you exactly where users drop off.

  • Click the + tab at the top of your Exploration (next to your current tab name) to add a new tab.
  • In the Tab Settings panel, click the Technique dropdown.
  • Select Funnel exploration.

The canvas changes to show a funnel with placeholder steps. You need to define the four steps of your user journey.

  • Click the pencil icon next to Steps in the Tab Settings panel.
  • Click Add new step.
  • Name this step Page View.
  • Click Add new condition.
  • Set the condition to: event name exactly matches page_view.

Repeat this process for the remaining three steps.

  • Add Step 2: name it CTA Click, condition event name exactly matches click_cta.
  • Add Step 3: name it View Pricing, condition event name exactly matches view_pricing.
  • Add Step 4: name it Select Plan, condition event name exactly matches select_plan.
  • Click Apply.

The funnel visualization appears, showing horizontal bars that narrow at each step. Each bar shows the number of users who completed that step and the percentage who dropped off before the next one.

What does the funnel tell you?

The funnel maps out the user journey you designed with your four custom events. The drop-off percentages between steps reveal where users lose interest or get stuck.

For example, a large drop-off between CTA Click and View Pricing might suggest that the pricing section is too far down the page. A PM would use this data to prioritize a layout change.

Why does GA4 apply funnels to historical data?

Unlike some older analytics tools, GA4 funnel Explorations apply retroactively to data that has already been collected. You did not need to define this funnel before generating your events. GA4 stored the raw events and lets you query them in any order after the fact.

Funnel showing zero users at every step?

Double check that each step condition uses the exact event name. Event names in GA4 are case-sensitive. page_view is correct, but Page_View would not match.

If the event names are correct but the funnel is still empty, your data may not have finished processing yet. Wait 15 to 30 minutes and refresh the Exploration.

Help me troubleshoot why my GA4 funnel Exploration shows zero users

Screenshot both Explorations

These two Explorations are the visual evidence of your analytics skills. A PM portfolio with actual dashboards is far more convincing than a bullet point that says "proficient in GA4."

  • Click on the first tab in your Exploration to switch back to the free-form bar chart.
  • Take a screenshot of the bar chart. Press Cmd+Shift+4 (macOS) or Windows+Shift+S (Windows) to select the chart area.
  • Save the screenshot to a folder on your Desktop.
  • Click on the second tab to switch to the funnel visualization.
  • Take a screenshot of the funnel chart using the same shortcut.
  • Save this screenshot alongside the first one.

Name your files clearly

Use descriptive filenames like ga4-event-bar-chart.png and ga4-funnel-visualization.png. When you add these to your portfolio, clear names make a good impression.

You now have two portfolio-ready Explorations that visualize your product data. Next up, you will turn these numbers into a written product recommendation that shows you can go from data to decisions.

Write Your Product Recommendation

You now have two polished Explorations showing real event data and a funnel visualization of the user journey. These are strong visual artifacts, but a hiring manager wants to see what you do with data, not just that you collected it.

In this step, you will write a short product recommendation document that turns your analytics into a shipping decision. This is the artifact that separates a technical exercise from a PM portfolio piece.

In this step, get ready to:
  • Create a recommendation document with a clear four-section structure.
  • Write a data-backed analysis of your funnel drop-off results.
  • Connect your findings to core PM skills that hiring managers look for.
Create the recommendation document

Every good product recommendation follows a predictable structure. Hiring managers skim dozens of these. A clear format lets them find your insight quickly.

  • Open a new document in Google Docs, Notion, or any text editor you prefer.
  • Add the document title: TaskFlow Landing Page: Analytics Findings and Recommendation
  • Below the title, add today's date.
  • Add your name as the author.
  • Create four section headers: Context, Data Summary, Analysis, and Recommendation.

Why this structure?

This four-section format mirrors how PMs communicate product decisions at companies like those you are applying to. Context sets the scene. Data Summary shows your evidence. Analysis proves you can think critically. Recommendation proves you can ship.

Fill in each section

Each section has a specific job. You will work through them one at a time, pulling data directly from the Explorations you built in the previous step.

  • In the Context section, write 2-3 sentences explaining what you measured and why.
  • Mention that you instrumented a SaaS landing page with four custom GA4 events to track the user journey from initial page view through plan selection.

Here is an example of what your Context section might look like. Try writing it yourself first.

✔️ I'll give it a go!

Write your own Context paragraph using the guidance above. Move on to the Data Summary section when you are done.

ⓧ Maybe a little help...

Here is an example you can adapt:

Example Context section

"TaskFlow is a fictional SaaS project management tool. I built a product landing page and instrumented it with four custom GA4 events to measure how users move from initial interest to plan selection. The goal was to identify where users drop off in the conversion funnel and recommend a specific product change to improve conversion."

  • In the Data Summary section, create a table with three columns: Event Name, Event Count, and Users.
  • Open your Free-Form Exploration in GA4 (click Explore in the left sidebar, then select your exploration).
  • Copy the event names and their corresponding counts and user numbers from your bar chart into the table.
  • Your table should include rows for page_view, click_cta, view_pricing, select_plan, and submit_newsletter.

Use your actual numbers

Your event counts will be small since you generated the data yourself. That is completely fine. The point is demonstrating the process, not having large sample sizes. Use whatever numbers your Exploration shows.

  • In the Analysis section, identify the biggest drop-off in your funnel.
  • Open your Funnel Exploration tab in GA4 to see the drop-off percentages between each step.
  • Write 2-3 sentences naming which step has the largest drop-off and hypothesizing why users leave at that point.

The hypothesis is the most important part of the Analysis section. It shows you can think beyond the numbers.

✔️ I'll give it a go!

Write your own Analysis using the funnel data. Move on to the Recommendation section when you are done.

ⓧ Maybe a little help...

Here is an example you can adapt based on your own funnel data:

Example Analysis section

"The largest drop-off occurs between the CTA click and pricing view steps. Users who click 'Start Free Trial' are expressing intent, but many never scroll down to see the pricing options. This suggests the page layout creates a disconnect: the CTA captures interest, but the features section between the hero and pricing acts as a friction point that loses engaged users."

  • In the Recommendation section, propose one specific change to the product based on your analysis.
  • Include the expected impact of your proposed change (e.g., "reduce the CTA-to-pricing drop-off by 20%").
  • Name one metric you would track to measure whether the change succeeded.

What makes a strong recommendation?

A strong PM recommendation is specific and measurable. "Improve the page" is vague. "Move the pricing section directly below the hero to reduce the CTA-to-pricing drop-off, measured by the view_pricing completion rate" is concrete and testable.

Try writing your Recommendation, or check the example if you would like a reference.

✔️ I'll give it a go!

Write your own Recommendation using the guidance above. Move on to the final section when you are done.

ⓧ Maybe a little help...

Here is an example you can adapt:

Example Recommendation section

"I recommend moving the pricing section directly below the hero section, eliminating the features section as an intermediate step. Users who click 'Start Free Trial' are high-intent. Placing pricing immediately after the CTA should reduce the drop-off between CTA click and pricing view by approximately 20%."

"To measure success, I would track the view_pricing completion rate as a percentage of click_cta events over a two-week A/B test. If the rate increases by 15% or more, the change ships permanently."

Connect it to PM skills

Your document already proves you can work with data. The final paragraph makes the connection explicit for anyone reading your portfolio.

  • Below your Recommendation section, add a final paragraph titled What This Demonstrates.
  • In this paragraph, explain that the analysis demonstrates four core PM skills: instrumenting a product, defining metrics, identifying friction in a user journey, and making a data-backed prioritization call.
  • Keep this paragraph to 2-3 sentences.

Try writing it yourself, or check the example below.

✔️ I'll give it a go!

Write your own closing paragraph. Save the document when you are done.

ⓧ Maybe a little help...

Example closing paragraph

"This project demonstrates end-to-end product analytics skills: I instrumented a product with custom event tracking, defined key metrics tied to business outcomes, identified the highest-friction point in the user journey using funnel analysis, and proposed a specific, measurable change backed by data. These are the core skills behind quantitative decision-making in a PM role."

  • Save your completed document.

Why does this closing paragraph matter?

PM hiring managers look for communication skills as much as tool proficiency. The strongest portfolios show data-backed decision-making with specific metrics. This closing paragraph is the "so what" that connects a technical exercise to the job you are applying for.

Secret mission

Add a Mobile vs. Desktop Segment

Your funnel shows where users drop off, but it treats all users the same. In this extension, you will segment your funnel by device category to compare mobile and desktop behavior. This is a core PM skill for identifying platform-specific product opportunities.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and uses the free tier of GA4, so there are no ongoing costs.

Resources you used:

  • GA4 account ("Portfolio Projects") with the "TaskFlow Landing Page" property and web data stream.
  • GA4 Explorations (free-form bar chart and funnel visualization).
  • Local index.html file served by Live Server in VS Code.
  • Product recommendation document (Google Docs, Notion, or text editor).
  • VS Code with the Live Server extension.

Keep everything running

No action needed. Choose this if you are still actively building on your analytics case study or plan to add more events and Explorations.

  • Your GA4 property continues collecting data whenever you open the page with Live Server.
  • Your Explorations and key event configuration stay saved in your GA4 property.
  • Your local index.html and recommendation document remain on your computer.

Pause - I'll come back to this later

Stop Live Server to free up the local port. Your GA4 property and files stay intact for when you return.

  • In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
  • Your GA4 property, Explorations, and key event settings are saved automatically. No action needed there.
  • Keep your index.html file and recommendation document saved locally.
  • When you come back, right-click index.html in VS Code and select Open with Live Server to pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps to clean everything up.

  • In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
  • Delete the index.html file from your computer.
  • Delete your product recommendation document if you no longer need it.
  • To delete the GA4 property, go to Admin in GA4, select your TaskFlow Landing Page property, click Property Settings, then click Move to Trash Can.
  • To delete the GA4 account entirely, go to Admin, click Account Settings under the Portfolio Projects account, then click Move to Trash Can.
  • To uninstall the Live Server extension, open the Extensions panel in VS Code, find Live Server, and click Uninstall.

Mission accomplished

Nice Work!

Congratulations! You just built a complete PM analytics portfolio piece from scratch. You can now point a hiring manager to a real case study that shows you can instrument a product, read the data, and make a recommendation.

Here's what you accomplished:

  • Set up a GA4 property end-to-end with a data stream, custom events, and DebugView verification, and instrumented a landing page with four events that track a meaningful user journey.
  • Built two Explorations (a free-form bar chart and a funnel visualization) that turn raw event data into portfolio-ready visuals.
  • Wrote a data-backed product recommendation that identifies friction in the user journey and proposes a prioritized next step with an expected impact and success metric.
  • Secret Mission: Added mobile vs. desktop segmentation to your funnel analysis and surfaced a platform-specific insight in your recommendation.

Ready to quiz yourself?