Skip to content

NagaYu/edge-gtm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⚑ EdgeGTM

Run server-side Google Tag Manager (sGTM) on the Cloudflare edge for $0–$5/month β€” with first-party cookies that survive Safari ITP, baked in.

Deploy to Cloudflare Workers

License: MIT Cloudflare Workers TypeScript Tests PRs welcome


Why does this exist?

Server-side Google Tag Manager is the modern, privacy-respecting way to collect analytics β€” but Google's reference deployment runs on Google Cloud App Engine / Cloud Run, which means:

  • πŸ’Έ You pay for always-on instances. A "small" sGTM setup is typically 3+ App Engine instances, landing most teams at $40–$120+/month before a single extra visitor shows up. Scale it for real traffic and the bill climbs fast.
  • 🐌 One region, added latency. Your tagging server lives in a single GCP region. A shopper in Tokyo hitting a server in us-central1 eats a round-trip they didn't need.
  • πŸ”§ It's infrastructure you now babysit β€” instances, scaling config, SSL, uptime.

EdgeGTM flips the model. It's a tiny Cloudflare Worker that acts as the first-party front door to your sGTM container and runs in 330+ cities on Cloudflare's edge network:

Classic GCP sGTM EdgeGTM on Cloudflare
Monthly cost ~$40–$120+ (always-on instances) $0 (Free: 100k req/day) β†’ $5 (Paid: 10M req/mo)
Latency Single region Anycast edge, 330+ cities
Cold starts Possible None (V8 isolates, ~0ms)
Scaling You configure instances Automatic, to millions of req
ITP cookie fix DIY Built in
Ops burden Instances, SSL, scaling One wrangler deploy

πŸ’‘ You can even point EdgeGTM at a free sGTM preview/tagging server or your own backend that speaks the sGTM protocol, and let the edge handle delivery, TLS, and cookie hardening.


The ITP problem (and how EdgeGTM solves it)

Safari's Intelligent Tracking Prevention (ITP) caps any cookie written by client-side JavaScript (document.cookie) to 7 days β€” and as little as 24 hours for visitors arriving with tracking parameters. Firefox and Brave do similar things. The damage:

  • πŸ” Returning customers are miscounted as brand-new visitors.
  • πŸ“‰ Attribution windows collapse, so your ad spend looks worse than it is.
  • 🧩 Cross-session journeys (browse today β†’ buy in 10 days) break.

ITP leaves exactly one door open: cookies set by a real first-party server via the Set-Cookie HTTP header are honored for their full lifetime. EdgeGTM runs on your own subdomain (e.g. sgtm.example.com) and intercepts every Set-Cookie coming back from the container, rewriting it into a hardened first-party cookie:

Set-Cookie: FPID=xxxx; Domain=.example.com; Path=/; Max-Age=34560000;
            Expires=...; SameSite=Lax; Secure; HttpOnly
  • πŸͺ Domain=.example.com β†’ genuine first party, shared across subdomains
  • ⏳ Max-Age extended to 400 days β†’ no more 7-day amnesia
  • πŸ”’ SameSite=Lax; Secure; HttpOnly β†’ safe defaults, invisible to XSS/document.cookie

The identity cookies it extends include FPID, FPLC, _ga, _ga_*, _gcl*, and friends.


Data flow

flowchart LR
    subgraph Browser["πŸ§‘ Visitor's Browser"]
      A["gtag.js / GTM<br/>fires /g/collect"]
    end

    subgraph Edge["⚑ Cloudflare Edge (your subdomain)"]
      W["EdgeGTM Worker<br/>sgtm.example.com"]
    end

    subgraph Origin["☁️ Server-side GTM Container"]
      G["sGTM tagging server<br/>(Cloud Run / preview / backend)"]
    end

    subgraph Vendors["πŸ“Š Destinations"]
      V1["GA4"]
      V2["Google Ads"]
      V3["Meta / TikTok / ..."]
    end

    A -- "1 first-party request<br/>(cookies attached)" --> W
    W -- "2 sanitized proxy<br/>+ X-Forwarded-For<br/>+ preview header" --> G
    G -- "3 response<br/>+ Set-Cookie (3rd-party-ish)" --> W
    W -- "4 rewritten Set-Cookie<br/>(1st-party, 400d, Lax/Secure/HttpOnly)" --> A
    G -.-> V1
    G -.-> V2
    G -.-> V3
Loading
  1. The browser fires its measurement request to your subdomain β€” a first-party call.
  2. EdgeGTM strips hop-by-hop and Cloudflare-internal headers, forwards the original client IP and the GTM preview header, and proxies to your container.
  3. The container processes the hit and returns its Set-Cookie headers.
  4. EdgeGTM rewrites those cookies into durable first-party cookies and streams the response back to the browser.

πŸš€ 10-second quick start

# 1. Clone & install
git clone https://github.com/NagaYu/edge-gtm.git
cd edge-gtm
npm install

# 2. Point it at your sGTM container + your domain
#    Edit wrangler.toml:
#      GTM_CONTAINER_URL = "https://your-sgtm-container.run.app"
#      COOKIE_DOMAIN     = ".yourdomain.com"

# 3. Ship it to the edge
npm run deploy

That's it. Your Worker is live at https://edge-gtm.<your-subdomain>.workers.dev.

Make it truly first-party (recommended)

Map a subdomain of your site to the Worker so cookies are first-party. In wrangler.toml:

[[routes]]
pattern = "sgtm.yourdomain.com/*"
zone_name = "yourdomain.com"

Then set your GTM/gtag server_container_url (or transport_url) to https://sgtm.yourdomain.com. Redeploy with npm run deploy.


βš™οΈ Configuration

All settings live in wrangler.toml under [vars] (and [env.production.vars]). Secrets should use wrangler secret put <NAME> instead.

Variable Required Default Description
GTM_CONTAINER_URL βœ… β€” Upstream sGTM origin (scheme + host, no trailing slash/path).
COOKIE_DOMAIN ⬜ (none) Registrable domain for first-party cookies, e.g. .example.com.
COOKIE_MAX_AGE_DAYS ⬜ 400 Lifetime applied to identity cookies (FPID/FPLC/_ga/…).
GTM_PREVIEW_HEADER ⬜ (empty) Value for X-Gtm-Server-Preview to keep Preview/Debug working.
EXTRA_PROXY_PATHS ⬜ (empty) Comma-separated extra path prefixes to proxy.
FORWARD_CLIENT_IP ⬜ true Forward the real client IP via X-Forwarded-For for geo/IP enrichment.
LOG_LEVEL ⬜ info debug | info | warn | error.

Proxied endpoints

Out of the box EdgeGTM proxies the standard sGTM endpoints, including:

/g/collect   /j/collect   /r/collect   /collect
/gtag/js     /gtag/destination          /gtm.js
/td          /ccm/collect /pagead/      /dc/      /healthz

Need more? Add them to EXTRA_PROXY_PATHS. Anything not on the allowlist returns 404, so the Worker can never be abused as an open proxy.


πŸ› οΈ Local development

npm run dev          # wrangler dev β€” local edge runtime at http://localhost:8787
npm test             # vitest β€” cookie rewriting + proxy behavior (39 tests)
npm run test:watch   # vitest in watch mode
npm run typecheck    # strict TypeScript, no emit
npm run tail         # live-stream structured production logs

Hit a local endpoint to sanity-check the proxy:

curl -i "http://localhost:8787/g/collect?v=2&tid=G-XXXX&en=page_view"

You should see the upstream status echoed, an x-edge-gtm: 1 header, and any cookies rewritten to SameSite=Lax; Secure; HttpOnly.

A built-in liveness probe is always available (never proxied):

curl -s http://localhost:8787/__edgegtm/health
# {"status":"ok","upstream":"your-sgtm-container.run.app"}

🧱 Architecture

edge-gtm/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.ts        # Entry: config parse, path allowlist, proxy, error handling
β”‚   β”œβ”€β”€ cookie.ts       # Set-Cookie parsing + first-party / ITP cookie rewriting
β”‚   └── types.ts        # Env, RuntimeConfig, header denylists, endpoint allowlist
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ cookie.test.ts  # Cookie parsing/splitting/rewriting (22 tests)
β”‚   └── proxy.test.ts   # Config parsing + full fetch-handler proxy (17 tests)
β”œβ”€β”€ .github/workflows/
β”‚   └── ci.yml          # Typecheck β†’ test β†’ dry-run build on push/PR
β”œβ”€β”€ wrangler.toml       # Worker name, compatibility, env vars, routes
β”œβ”€β”€ tsconfig.json       # Strict TS config targeting the Workers runtime
β”œβ”€β”€ vitest.config.ts    # Test runner config
β”œβ”€β”€ .dev.vars.example   # Local secrets/vars template (copy to .dev.vars)
β”œβ”€β”€ package.json        # Dev deps + dev/test/build/deploy scripts
β”œβ”€β”€ LICENSE             # MIT
└── README.md

Design principles

  • 🌐 Web Standards only β€” Request / Response / Headers / fetch. No Node-isms in the hot path.
  • πŸ›‘οΈ Defensive by default β€” strict header denylists (hop-by-hop + Cloudflare-internal stripped), explicit method gate, allowlisted endpoints, and upstream failures mapped to clean 502s instead of leaking stack traces.
  • πŸ”’ Type-safe β€” strict TypeScript with noUncheckedIndexedAccess and exactOptionalPropertyTypes; no any.
  • πŸͺ Spec-correct cookies β€” a comma-safe Set-Cookie splitter (so Expires=Wed, 09 Jun … never breaks), getSetCookie() when available, and __Host- prefix handling.

❓ FAQ

Does this replace my sGTM container? No β€” it's the first-party edge front door to it. Your container still does the tagging; EdgeGTM makes delivery fast, cheap, and ITP-resistant.

Will it work with GA4 / Google Ads / Meta CAPI tags? Yes. EdgeGTM is transport-layer; it forwards whatever your container speaks and only rewrites cookies on the way back.

Is forwarding the client IP a privacy problem? Set FORWARD_CLIENT_IP = "false" to strip it. By default it's forwarded so server-side geo/IP enrichment stays accurate β€” your call.

Can I keep using GTM Preview/Debug mode? Yes β€” set GTM_PREVIEW_HEADER to the value from your container's Preview screen.


🀝 Contributing

Issues and PRs are very welcome. Please run npm run typecheck before opening a PR.


πŸ“„ License

MIT Β© EdgeGTM contributors.

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Built for marketers and engineers who refuse to pay $100/month to count a click. ⚑

About

Run server-side Google Tag Manager (sGTM) on Cloudflare Workers for $0-$5/mo, with first-party cookie rewriting that survives Safari ITP.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors