Claude Code Analytics Implementation: GA4, GSC, Cloudflare, PV, and Revenue
Build Claude Code analytics with GA4, GSC, Cloudflare, consent, PV, CTA, revenue events, and checks.
Analytics Implementation Starts Before the Tag
Analytics implementation is the work of turning page views, clicks, article completion, inquiries, product clicks, and purchases into data that can guide decisions. A PV is a page view. An event is a recorded action, such as a CTA click or a successful form submission. A conversion, or GA4 key event, is an event the business treats as an outcome. UTM parameters are campaign labels in the URL. Consent is the rule that decides whether browser analytics may send data.
Claude Code is useful here because it can turn a measurement plan into code, tests, documentation, and review prompts in one pass. The mistake Masa made on this site was watching PV growth while not separating product-link clicks, lead-form completion, and free-resource signups. Traffic looked healthy, but the revenue path was invisible. That forced a later cleanup of event names, dashboards, and CTAs.
This guide focuses on a practical stack: GA4 for campaign and key-event reporting, Search Console for query and page demand, Cloudflare for edge-level request signals, Plausible for lightweight goals, and PostHog for funnels or product behavior. Pair it with SEO optimization, A/B testing, performance optimization, and content funnel audits.
Write the Measurement Plan First
Start by asking Claude Code for a table that works backward from decisions, not from tools.
Create an analytics implementation plan for this content site.
The goal is not only PV growth, but article completion, CTA clicks, inquiries, product clicks, and purchase-path improvement.
Use columns business_question, event_name, trigger, required_params, provider, decision.
Prefer GA4 recommended events where they fit. Use snake_case for custom events.
| business_question | event_name | trigger | required_params | provider | decision |
|---|---|---|---|---|---|
| Are articles read to the end? | article_read_complete | Article footer is 70% visible | slug, category, reading_time_sec | GA4/PostHog | Rewrite intro, headings, internal links |
| Are CTAs clicked? | cta_click | Product, training, or free PDF CTA clicked | slug, cta_id, cta_type, target_url | GA4/Plausible/PostHog | Change CTA placement and copy |
| Did an inquiry complete? | generate_lead | Form submission succeeds | form_id, lead_source, value, currency | GA4/PostHog | Fix form and offer clarity |
| Do product links create buying intent? | purchase_link_click | Product page or Gumroad link clicked | product_id, price, currency, slug | GA4/PostHog | Match articles to products |
| Which search queries have value? | gsc_query_page | Search Console API returns page and query data | page, query, clicks, impressions, ctr, position | GSC | Pick titles and updates |
| Are browser tags missing traffic? | edge_page_view | Cloudflare Worker receives a request | path, country, status, duration_ms | Cloudflare | Detect blockers and speed issues |
GA4 recommended events should be checked against Google’s recommended events. Use generate_lead or purchase when the meaning fits. Use custom names only when the action is specific to the article or funnel.
flowchart LR
Reader["Reader"]
Consent["Consent state"]
Browser["browser analytics.js"]
Server["GA4 Measurement Protocol"]
GSC["Search Console API"]
Edge["Cloudflare Worker"]
Dashboard["Editorial, revenue, and quality dashboards"]
Reader --> Consent --> Browser
Browser --> Server
GSC --> Dashboard
Edge --> Dashboard
Browser --> Dashboard
Server --> Dashboard
Keep the Event Contract in JavaScript
An event contract is the shared promise for event names, required parameters, and providers. Put it in a file that Claude Code can import or update, then ask it to modify tests whenever a new event is added.
// event-plan.mjs
import { pathToFileURL } from "node:url";
export const eventPlan = {
article_read_complete: {
required: ["slug", "category", "reading_time_sec"],
providers: ["GA4", "PostHog"],
},
cta_click: {
required: ["slug", "cta_id", "cta_type", "target_url"],
providers: ["GA4", "Plausible", "PostHog"],
},
generate_lead: {
required: ["form_id", "lead_source", "value", "currency"],
providers: ["GA4", "PostHog"],
},
purchase_link_click: {
required: ["product_id", "price", "currency", "slug"],
providers: ["GA4", "PostHog"],
},
campaign_landing: {
required: ["utm_source", "utm_medium", "utm_campaign"],
providers: ["GA4"],
},
};
export function validateEvent(name, params = {}) {
const contract = eventPlan[name];
if (!contract) return { ok: false, missing: ["known_event_name"] };
const missing = contract.required.filter((key) => params[key] === undefined || params[key] === "");
return { ok: missing.length === 0, missing };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const result = validateEvent("cta_click", {
slug: "claude-code-analytics-implementation",
cta_id: "products_footer",
cta_type: "product",
target_url: "/en/products/",
});
console.log(result);
}
The two monetization CTAs should not be merged. A click to products means the reader wants a template, guide, or downloadable resource. A click to training and consultation means the reader may need team rollout help. They need different cta_type values and different dashboard owners.
Browser Event Layer
The browser layer should own consent, UTM storage, parameter cleanup, and provider fan-out. Do not scatter direct gtag calls across components; that is how consent bugs and naming drift start.
// browser-analytics.js
const CONSENT_KEY = "analytics_consent";
const UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
function inBrowser() {
return typeof window !== "undefined" && typeof localStorage !== "undefined";
}
function hasConsent() {
return inBrowser() && localStorage.getItem(CONSENT_KEY) === "granted";
}
function cleanParams(params = {}) {
return Object.fromEntries(
Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== "")
.map(([key, value]) => [key, typeof value === "boolean" ? Number(value) : value])
);
}
export function setAnalyticsConsent(state) {
if (!inBrowser()) return;
localStorage.setItem(CONSENT_KEY, state);
window.gtag?.("consent", "update", {
analytics_storage: state,
ad_storage: "denied",
});
}
export function readUtmParams() {
if (!inBrowser()) return {};
const current = new URLSearchParams(window.location.search);
const saved = JSON.parse(localStorage.getItem("landing_utm") || "{}");
const next = { ...saved };
for (const key of UTM_KEYS) {
const value = current.get(key);
if (value) next[key] = value;
}
localStorage.setItem("landing_utm", JSON.stringify(next));
return next;
}
export function trackEvent(name, params = {}) {
if (!hasConsent()) return;
const payload = cleanParams({ ...readUtmParams(), ...params });
window.gtag?.("event", name, payload);
window.plausible?.(name, { props: payload });
window.posthog?.capture(name, payload);
}
export function trackCtaClick({ slug, ctaId, ctaType, targetUrl }) {
trackEvent("cta_click", {
slug,
cta_id: ctaId,
cta_type: ctaType,
target_url: targetUrl,
});
}
Track article completion with IntersectionObserver when the article footer becomes visible. Send generate_lead after the form succeeds, not when the submit button is clicked. A clicked button can still fail validation, lose network connectivity, or be abandoned.
GA4 Server Events and Validation
Use server events for outcomes confirmed outside the browser: purchases, webhook confirmations, and completed inquiry records. GA4 Measurement Protocol is documented in the developer guide, and the validation server should be used before production because malformed Measurement Protocol events may still return a normal HTTP response.
// ga4-server-event.mjs
import { pathToFileURL } from "node:url";
const { GA4_MEASUREMENT_ID, GA4_API_SECRET, GA4_DEBUG } = process.env;
if (!GA4_MEASUREMENT_ID || !GA4_API_SECRET) {
throw new Error("GA4_MEASUREMENT_ID and GA4_API_SECRET are required");
}
export async function sendGa4Event({ clientId, name, params = {} }) {
const endpoint = new URL(
GA4_DEBUG === "1"
? "https://www.google-analytics.com/debug/mp/collect"
: "https://www.google-analytics.com/mp/collect"
);
endpoint.searchParams.set("measurement_id", GA4_MEASUREMENT_ID);
endpoint.searchParams.set("api_secret", GA4_API_SECRET);
const response = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
client_id: clientId,
events: [{ name, params }],
}),
});
if (!response.ok) {
throw new Error("GA4 request failed with status " + response.status);
}
if (GA4_DEBUG === "1") {
const result = await response.json();
if (result.validationMessages?.length) {
throw new Error(JSON.stringify(result.validationMessages, null, 2));
}
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await sendGa4Event({
clientId: "555.1234567890",
name: "generate_lead",
params: {
form_id: "training",
lead_source: "article_footer",
value: 1,
currency: "USD",
},
});
console.log("sent");
}
The pitfall is double counting. If the server owns a conversion, the browser should only send intent, such as cta_click. If the browser owns a form completion, the server event should be a backup with a deduplication key, not a second conversion.
Search Console Query Import
Search Console answers a different question from GA4: which queries and pages create search demand. Use the Search Analytics API to pull pages and queries, then compare them with article completion and CTA behavior.
// gsc-query.mjs
import { pathToFileURL } from "node:url";
const { GSC_ACCESS_TOKEN, GSC_SITE_URL = "https://example.com/" } = process.env;
if (!GSC_ACCESS_TOKEN) {
throw new Error("GSC_ACCESS_TOKEN is required");
}
export async function querySearchConsole({ startDate, endDate, pageContains }) {
const endpoint =
"https://www.googleapis.com/webmasters/v3/sites/" +
encodeURIComponent(GSC_SITE_URL) +
"/searchAnalytics/query";
const response = await fetch(endpoint, {
method: "POST",
headers: {
authorization: "Bearer " + GSC_ACCESS_TOKEN,
"content-type": "application/json",
},
body: JSON.stringify({
startDate,
endDate,
dimensions: ["page", "query"],
dimensionFilterGroups: pageContains
? [{ filters: [{ dimension: "page", operator: "contains", expression: pageContains }] }]
: [],
rowLimit: 25,
}),
});
if (!response.ok) {
throw new Error("Search Console request failed with status " + response.status);
}
return response.json();
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const data = await querySearchConsole({
startDate: "2026-05-01",
endDate: "2026-05-31",
pageContains: "/blog/claude-code-analytics-implementation",
});
console.log(JSON.stringify(data.rows ?? [], null, 2));
}
Do not treat GSC as a perfect log. It returns aggregated search performance and is affected by Search Console limits. Use it for trend decisions: rewrite titles when impressions are high and CTR is low, expand sections when the query intent is clear but article completion is weak, and prioritize pages that already have demand.
Cloudflare Edge Events
Cloudflare can provide a second view of traffic when browser tags are blocked or delayed. Workers Analytics Engine is designed for custom aggregated data; Cloudflare documents the binding and the writeDataPoint pattern in Workers Analytics Engine and the Workers example.
// cloudflare-worker.js
function json(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json" },
});
}
export default {
async fetch(request, env) {
if (request.method !== "POST") {
return json({ ok: false, error: "method_not_allowed" }, 405);
}
const event = await request.json().catch(() => null);
if (!event?.event_name || !event?.slug) {
return json({ ok: false, error: "event_name_and_slug_required" }, 400);
}
const country = request.cf?.country || request.headers.get("cf-ipcountry") || "XX";
env.ANALYTICS?.writeDataPoint({
blobs: [event.event_name, event.slug, event.cta_id || "", country],
doubles: [Number(event.value || 1)],
indexes: [String(event.slug).slice(0, 96)],
});
return json({ ok: true });
},
};
Keep personally identifiable information out of this layer. Store event names, slugs, status groups, timing, and country-level context. Do not store email addresses, names, free-form inquiry text, or raw IP addresses in event blobs.
Three Dashboards
Build three small dashboards instead of one crowded page. The editorial dashboard uses PV, GSC query/page data, read completion, and internal-link clicks. The revenue dashboard uses cta_click, purchase_link_click, generate_lead, product IDs, prices, and currencies. The quality dashboard uses missing events, consent rate, edge-versus-browser gaps, and Core Web Vitals risk.
This split prevents a common failure: editorial work celebrates PV growth while the business still cannot see whether readers reach products or training. It also makes ownership clear. Editors fix titles and structure. Marketing fixes CTAs. Engineering fixes consent, script loading, duplicate conversions, and event contract drift.
Monetization and PV Use Cases
Use case 1: SEO content. If GSC impressions are high but CTR is low, rewrite title and description. If GA4 PV is high but article_read_complete is low, the intro, heading order, or code example may not match the search intent.
Use case 2: products. Track products clicks with purchase_link_click, including product_id, price, currency, and slug. A tutorial that creates many product clicks deserves a clearer product card or comparison table.
Use case 3: training and consultation. Track training clicks as cta_click, then track successful form submission as generate_lead. When clicks are high but leads are low, fix the form length, mobile layout, or offer copy before buying more traffic.
Use case 4: campaigns. Store UTM parameters on the first landing page so the final lead can still carry the campaign source. This matters when readers visit several pages before submitting a form.
Concrete Pitfalls
The first pitfall is event-name drift. ctaClick, cta_click, and button_click become three separate reporting problems. Use the event contract as the single source of truth.
The second pitfall is consent leakage. Browser analytics should default to no send until consent exists. Consent rules vary by market and advertising use, so the code should be conservative and the privacy policy should match reality.
The third pitfall is duplicate server and browser conversions. Decide the owner for each event. Purchases usually belong to the server or webhook. CTA intent belongs to the browser.
The fourth pitfall is over-instrumentation. Every new provider adds script weight, review work, and privacy surface area. Plausible is often enough for lightweight goals. PostHog is justified when funnels, recordings, or product behavior actually change decisions.
Rollout Checklist
- The measurement plan names events, triggers, required params, providers, owners, and decisions
- GA4 recommended events are used where they fit
- GSC query/page data is compared with article completion and CTA data
- Cloudflare events do not contain personal data
- Browser events are blocked before consent
- UTM parameters persist from first landing to lead completion
- Client and server conversions are not double-counted
- GA4 DebugView, Realtime, Plausible Goals, PostHog Events, and Cloudflare aggregates are checked within 24 hours
A practical implementation review can start from the products path for templates and the training and consultation path for team rollout. The point is not to add more events; it is to leave only the events that change product, content, or revenue decisions.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.