Plain is headed towards 1.0! Subscribe for development updates →

Google Analytics 4 across subdomains

Guide by @davegaeddert • 2023

Set up Google Analytics 4 on multiple subdomains with a single property.

Often times with Plain or Django SaaS apps, you'll have multiple subdomains running different frameworks for different purposes.

For example, if your Plain app is app.example.com on Heroku, you might have static marketing site at www.example.com built with a completely different stack. Similar things happen with blog.example.com or docs.example.com.

In my experience, the easiest way to use Google Analytics across subdomains like this is to use a single property and tracking code. Then you can see how people move across your marketing site into your app, docs, blog, etc.

There are a few tricks to getting this set up though.

Create a GA4 property

If you already have a Universal Analytics (UA) property, you can use the "GA4 Setup Assistant". It will copy over a few basic things and add "- GA4" to the name.

If you aren't using UA, just create a new property with a "web" stream. When it asks for a url/domain, just use your primary "www.example.com" domain (it will still track all the different subdomains).

As far as naming goes, I tend to use something like "PullApprove domains" (for PullApprove) to make it clear that the property is used across subdomains.

Set up Google Tag Manager

If you are using the same tags across multiple subdomains, I'd strongly recommend using Google Tag Manager. With Google Tag Manager, you install that tag once on each site, and then future changes or additional tags can be edited directly in Tag Manager and deployed across the sites in one-click.

If you're new to Google Tag Manager, set up a new account/container to get your tag.

Here's an example of a simple way to install it in Django that excludes local development traffic (be sure to use your own tag ID):

<!-- base.template.html -->
<!doctype html>
<html lang="en">
  <head>
    {% if not debug %}
    <!-- Google Tag Manager -->
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
    <!-- End Google Tag Manager -->
    {% endif %}

    <!-- Regular head content here... -->
  </head>
  <body>
    {% if not debug %}
    <!-- Google Tag Manager (noscript) -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
    height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- End Google Tag Manager (noscript) -->
    {% endif %}
# settings.py
# https://docs.djangoproject.com/en/4.1/ref/settings/#internal-ips
# Adds the `debug` variable to the context when DEBUG is True.
INTERNAL_IPS = ["127.0.0.1"]

You can do something similar in most static site generators (like Combine), or your blog platform/host will probably have a way to add tracking tags.

Then add GA4 to your tag manager:

  1. Go to "Tags"
  2. Click "New" and add a "Google Analytics: GA4 Configuration" tag
  3. Add your "Measurement ID" from your new Google Analytics "stream"
  4. In "Fields to Set", add content_group with a value of {{Page Hostname}} (more about this later)
  5. Choose "All pages" as the trigger
  6. Save the tag and "Submit" your tag manager changes (button in the top right corner)

GA4 realtime report

Once everything is saved, committed, and deployed, you should be able to go back to Google Analytics and find the "Realtime" report. Open your live site in another tab, then wait to see if your traffic shows up!

(If you don't see any traffic, an easy first step is to "view source" on your site and see if tag manager is even there.)

Adding referral exclusions

Because we're working across subdomains, we want to make sure that our "acquisition" traffic isn't clogged up with "referrals" from our own sites. To do this we can add our top-level domain to the "List unwanted referrals" stream setting:

You can also add things like stripe.com here if you integrate with Stripe Checkout, for example. In most cases that isn't an acquisition source that should be tracked over the true, original source.

You don't need to do anything with "Configure your domains", which is for truly cross-domain tracking where the TLD isn't the same.

Page titles matter

One thing you'll notice in GA4 is that "page titles" and "screen names" are used all over the place instead of paths. The paths, hostnames, etc. data is still there, but the default views focus on page titles. So, don't forget to actually write page titles!

Forge comes with an HTMLTitleMixin that can actually raise an error if you forget to write page titles for a view.

Using content groups

Remember how we added content_group to our tag manager configuration? The value we gave it was {{Page Hostname}}, which means now we break down the page traffic into different "content groups".

Using hostname "comparisons"

When you're browsing the other standard reports, you can use the "comparison" feature to look at traffic for specific hostnames:

Basic conversions

One last key point worth mentioning is conversions. The general idea for conversions in GA4 is to create an "event", and then "mark as conversion".

Events can be created automatically based on page views and paths, for example.

Try to use the built-in event names (like "sign_up") when possible.