---
title: GTM vs. codebase tracking
description: Two ways to wire up analytics. They're not mutually exclusive, but the architectural choice matters.
readingTime: 11 min
---

# GTM vs. codebase tracking

> **Chapter 8 of the Concepts section.** Assumes you've read the earlier chapters in this section.

There are two ways to wire up analytics: install the SDK directly in your codebase, or route events through Google Tag Manager (GTM). They're not mutually exclusive — most production setups use both for different purposes — but the architectural choice matters.

## The problem GTM solves

Most SaaS products eventually need to send the same events to multiple destinations. You're tracking purchases in Amplitude for product analytics, in Facebook Pixel for ad attribution, in Google Ads for conversion tracking, in HubSpot for CRM. One business event, four destinations.

If you implement this directly in your codebase, your purchase handler ends up like this:

```ts
const handlePurchaseComplete = (orderData) => {
  // Amplitude
  amplitude.track('Purchase completed', {
    revenue: orderData.amount,
    orderId: orderData.id,
  });
  
  // Facebook Pixel
  fbq('track', 'Purchase', {
    value: orderData.amount,
    currency: orderData.currency,
  });
  
  // Google Ads
  gtag('event', 'purchase', {
    value: orderData.amount,
    currency: orderData.currency,
    transaction_id: orderData.id,
  });
  
  // HubSpot, and so on...
};
```

Four implementations of the same event. When marketing wants to add a fifth destination, an engineer has to write more code. When they want to add a property to one of the destinations, an engineer has to make the change. Every tracking adjustment becomes a code change, a code review, and a deploy.

## How GTM works

GTM introduces an intermediate layer. You install GTM's script on your site, which creates a `dataLayer` object. Your codebase pushes events to the `dataLayer` instead of directly to destinations:

```ts
const handlePurchaseComplete = (orderData) => {
  dataLayer.push({
    event: 'Purchase completed',
    revenue: orderData.amount,
    orderId: orderData.id,
    currency: orderData.currency,
  });
};
```

That's the whole code change. From there, marketing (or whoever owns analytics) configures GTM through its UI to listen for this event and forward it to Amplitude, Facebook, Google Ads, HubSpot, or any other destination. They can adjust properties, add filters, route to new destinations — all without code changes.

## When to use GTM

Use GTM when:

- You need to send events to **multiple destinations** (ad platforms, CRMs, email tools, plus your analytics tool)
- You want **non-engineers to manage tracking** without going through engineering
- You expect the destination list to **change over time** (new ad platforms, swapping CRMs)

Use codebase-only when:

- You're sending events to **one destination only** (just Amplitude, or just PostHog)
- You want **complete control in version control** and a clear audit trail
- Your team is engineering-led and there's no demand from marketing to manage tracking independently

For SaaS products beyond the earliest stage, GTM is the standard. It's what most teams converge on once they're integrating with even one marketing tool beyond their core analytics platform.

## The catch with GTM and server-side events

GTM lives in the browser. It can route client-side events to multiple destinations brilliantly. It can't help with server-side events.

Your revenue events (which need to be server-side for accuracy — see [Client-side vs. server-side tracking](./client-vs-server-tracking.md)) bypass GTM entirely. They go directly from your server to each destination. If you want a Stripe-triggered conversion event to reach both Amplitude and Facebook Ads, you're sending it twice from your server.

GTM has a feature called "server-side GTM" that addresses this — you run a GTM container on your own server that handles routing. It's an additional setup with its own infrastructure cost. For most teams it's overkill; you can just call each destination directly from your webhook handler.

## The hybrid that's typical

Most production setups look like this:

- **Client-side events** (page views, UI interactions, feature events) flow through GTM. Marketing manages the destination routing.
- **Server-side events** (revenue, subscription state changes, anything requiring 100% accuracy) go directly from your backend to each destination, called explicitly in your webhook handlers.

This gives you marketing flexibility for the events that benefit from it, and reliability for the events that need it. The downside is two parallel tracking pipelines to maintain, but in practice the server-side pipeline is small (just your webhook handlers) and the client-side pipeline is what most of the volume flows through anyway.

## A note on this guide's implementation chapters

The implementation chapters in this guide show direct codebase calls. If you're using GTM, the dataLayer pushes substitute for the SDK calls; the *triggers* (when to fire what event with what properties) are identical.

The reasoning behind those triggers — what to track, when, with what context — is what the concepts section covers. That doesn't change based on whether the calls go through GTM or not.
