Can you reliably see where your sign ups are coming from? Can you segment your funnel analytics based on these acquisition sources?
These questions are key to understanding which growth channels you should scrap and which ones you should double down on.
In this post, we’ll go over a reliable way to track this data within the common scenario of where your application code (and sign up page) is within a different codebase than your marketing site (ie. Webflow, Wordpress, another code repository, etc).
The easiest way to start tracking this data is by using a web analytics tool like PostHog, Amplitude, or Google Analytics. I’m a fan of the first two.
However, the reliability of browser-based third-party tracking scripts are diminishing. When using these kinds of tracking scripts, you can expect to lose a decent amount of signups for users using an ad blocker or even browsers like Brave or Arc.
For many businesses, each sign up attribution data point is valuable. A couple of use cases I’ve experienced:
To overcome the challenges associated with browser based tracking, implementing first party code to track visitor metadata before writing to a dedicated database table (signup_analytics
) proves to be an effective solution.
Database Table: Create a dedicated table in your database to record analytics data on those who’ve signed up. This table will tie each user to a signup analytics record, allowing for deeper analysis of activation rates based on acquisition referrer.
create table signup_analytics (
id serial primary key,
user_id uuid references public.users(id) on delete set null,
campaign text,
campaign_date timestamp with time zone,
source text,
source_date timestamp with time zone,
medium text,
medium_date timestamp with time zone,
term text,
term_date timestamp with time zone,
content text,
content_date timestamp with time zone,
referrer_url text,
referrer_url_date timestamp with time zone,
gclid text,
gclid_date timestamp with time zone,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now()
);
JavaScript Snippet for gathering visitor data: Implement a JavaScript snippet that tracks the visitor’s traffic metadata (utm parameters, google click ids, referrer url, etc) in a cookie as users navigate from one subdomain to another. For example, when users move from your marketing site (www.yourdomain.com) to the application site (app.yourdomain.com), both sites can check the same cookie for the visitor traffic metadata. This ensures that the metadata remains intact throughout the signup process.
NextJS Example: If you’re using NextJS for your marketing site, instantiate this context on every page.
"use client";
import {
createContext,
useContext,
useMemo,
useEffect,
useState,
useCallback,
} from "react";
import QueryString from "query-string";
import Cookies from "js-cookie";
import { SIGNUP_ANALYTICS_COOKIE_NAME } from "lib/config";
const getCookieDomain = () => {
if (typeof window !== "undefined") {
let hostname = window.location.hostname;
if (!hostname.includes("localhost")) {
// Get root domain
hostname =
"." +
hostname.split(".").reverse().splice(0, 2).reverse().join(".");
}
return hostname;
}
};
const isExpired = (daysUntilExpired) => (date) => {
const currentDate = new Date();
const expirationDate = new Date(date);
expirationDate.setDate(expirationDate.getDate() + daysUntilExpired);
return currentDate > expirationDate;
};
export const AnalyticsContext = createContext();
export const AnalyticsProvider = (props) => {
const [campaign, setCampaign] = useState({ value: null, date: null });
const [source, setSource] = useState({ value: null, date: null });
const [medium, setMedium] = useState({ value: null, date: null });
const [term, setTerm] = useState({ value: null, date: null });
const [content, setContent] = useState({ value: null, date: null });
const [gclid, setGclid] = useState({ value: null, date: null });
const [referrerUrl, setReferrerUrl] = useState({
value: null,
date: null,
});
const initFromCookie = () => {
const cookie = Cookies.get(SIGNUP_ANALYTICS_COOKIE_NAME);
if (cookie) {
const parsedCookie = JSON.parse(cookie);
if (parsedCookie.campaign?.value) {
setCampaign(parsedCookie.campaign);
}
if (parsedCookie.source?.value) {
setSource(parsedCookie.source);
}
if (parsedCookie.medium?.value) {
setMedium(parsedCookie.medium);
}
if (parsedCookie.term?.value) {
setTerm(parsedCookie.term);
}
if (parsedCookie.content?.value) {
setContent(parsedCookie.content);
}
if (parsedCookie.gclid?.value) {
setGclid(parsedCookie.gclid);
}
if (parsedCookie.referrerUrl?.value) {
setReferrerUrl(parsedCookie.referrerUrl);
}
}
};
const collectSignupAnalyticsFromUrl = () => {
const currentDate = new Date();
if (window.location?.search) {
const qs = QueryString.parse(window.location.search);
if (qs.utm_campaign !== null && qs.utm_campaign !== undefined) {
setCampaign({ value: qs.utm_campaign, date: currentDate });
}
if (qs.utm_source !== null && qs.utm_source !== undefined) {
setSource({ value: qs.utm_source, date: currentDate });
}
if (qs.utm_medium !== null && qs.utm_medium !== undefined) {
setMedium({ value: qs.utm_medium, date: currentDate });
}
if (qs.utm_term !== null && qs.utm_term !== undefined) {
setTerm({ value: qs.utm_term, date: currentDate });
}
if (qs.utm_content !== null && qs.utm_content !== undefined) {
setContent({ value: qs.utm_content, date: currentDate });
}
if (qs.gclid) {
setGclid({ value: qs.gclid, date: currentDate });
}
}
let referrerUrl;
if (document.referrer === "") {
referrerUrl = undefined;
} else {
try {
let parsedReferrer = new URL(document.referrer);
referrerUrl =
parsedReferrer.host.replace(/^www./, "") +
parsedReferrer.pathname;
} catch (e) {
referrerUrl = document.referrer;
}
}
if (referrerUrl) {
setReferrerUrl({ value: referrerUrl, date: currentDate });
}
};
const checkExpirations = useCallback(() => {
// config expiration in days
const isGclidExpired = isExpired(60);
if (gclid.date && isGclidExpired(gclid.date)) {
setGclid({ value: null, date: null });
}
}, [gclid.date]);
const syncCookie = useCallback(() => {
Cookies.set(
SIGNUP_ANALYTICS_COOKIE_NAME,
JSON.stringify({
campaign,
source,
medium,
term,
content,
gclid,
referrerUrl,
}),
{
domain: getCookieDomain(),
}
);
}, [campaign, source, medium, term, content, gclid, referrerUrl]);
const value = useMemo(
() => ({
campaign,
setCampaign,
source,
setSource,
medium,
setMedium,
term,
setTerm,
content,
setContent,
gclid,
setGclid,
referrerUrl,
setReferrerUrl,
}),
[campaign, source, medium, term, content, gclid, referrerUrl]
);
// init from cross domain cookie
useEffect(() => {
initFromCookie();
collectSignupAnalyticsFromUrl();
checkExpirations();
}, [checkExpirations]);
useEffect(() => {
syncCookie();
}, [value, syncCookie]);
return <AnalyticsContext.Provider value={value} {...props} />;
};
export const useAnalytics = () => {
const context = useContext(AnalyticsContext);
if (!context) {
throw new Error(
"useAnalytics must be used within a AnalyticsProvider"
);
}
return context;
};
Saving cookie data to your database: Upon signup, submit the captured cookie data to the signup analytics table. This step completes the tracking process and enables you to analyze user acquisition accurately. You’ll want to do this in the codebase that contains the sign up page.
Example using Supabase: This function is called whenever the user signs up successfully.
import Cookies from "js-cookie";
import { SIGNUP_ANALYTICS_COOKIE_NAME } from "@constants";
import { supabase } from "@/supabase";
export async function insertSignupAnalyticsFromCookie(userId: string) {
let saObj = {};
const cookie = Cookies.get(SIGNUP_ANALYTICS_COOKIE_NAME);
if (cookie) {
saObj = JSON.parse(cookie);
}
const { error } = await supabase.from("signup_analytics").insert([
{
user_id: userId,
campaign: saObj?.campaign?.value,
campaign_date: saObj?.campaign?.date,
source: saObj?.source?.value,
source_date: saObj?.source?.date,
medium: saObj?.medium?.value,
medium_date: saObj?.medium?.date,
term: saObj?.term?.value,
term_date: saObj?.term?.date,
content: saObj?.content?.value,
content_date: saObj?.content?.date,
referrer_url: saObj?.referrerUrl?.value,
referrer_url_date: saObj?.referrerUrl?.date,
gclid: saObj?.gclid?.value,
gclid_date: saObj?.gclid?.date,
},
]);
if (error) {
console.error(error);
return;
}
Cookies.remove(SIGNUP_ANALYTICS_COOKIE_NAME);
}
Analysis: Once you have this data, you’re all set to start reporting on your top acquisition sources and where you should double down.
Example SQL query:
SELECT
t1.email AS email,
t1.created_at,
CASE
WHEN t2.gclid IS NOT NULL THEN 'Google Ads'
WHEN t2.source IS NOT NULL AND t2.source ILIKE 'facebook%' THEN 'Facebook Ads'
WHEN t2.source IS NOT NULL AND t2.source ILIKE 'linkedin%' THEN 'LinkedIn Ads'
WHEN t2.source IS NOT NULL AND t2.source ILIKE 'instagram%' THEN 'Instagram Ads'
WHEN t2.source IS NOT NULL AND t2.source ILIKE 'twitter%' THEN 'Twitter Ads'
WHEN t2.source IS NOT NULL THEN t2.source
WHEN t2.referrer_url ILIKE 't.co%' THEN 'Twitter'
WHEN t2.referrer_url ILIKE 'l.facebook.com%' THEN 'Facebook'
WHEN t2.referrer_url ILIKE 'linkedin.com%' THEN 'LinkedIn'
WHEN t2.referrer_url ILIKE 'instagram.com%' THEN 'Instagram'
WHEN t2.referrer_url ILIKE 'pinterest.com%' THEN 'Pinterest'
WHEN t2.referrer_url ILIKE 'tiktok.com%' THEN 'TikTok'
WHEN t2.referrer_url ILIKE 'youtube.com%' THEN 'YouTube'
WHEN t2.referrer_url ILIKE 'baidu.com%' THEN 'Baidu'
WHEN t2.referrer_url ILIKE 'yahoo.com%' THEN 'Yahoo'
WHEN t2.referrer_url ILIKE 'bing.com%' THEN 'Bing'
WHEN t2.referrer_url IS NOT NULL AND
SPLIT_PART(t2.referrer_url, '/', 1) IS NOT NULL
THEN
SPLIT_PART(t2.referrer_url, '/', 1)
WHEN t2.referrer_url IS NOT NULL THEN 'Other Referrer'
ELSE 'Unknown'
END AS final_source,
COALESCE(t2.gclid, t2.source, t2.referrer_url) AS final_source_detail,
t2.campaign AS campaign,
CAST(DATE_TRUNC('day', t2.campaign_date) AS DATE) AS campaign_date,
t2.content AS content,
CAST(DATE_TRUNC('day', t2.content_date) AS DATE) AS content_date,
t2.gclid AS gclid,
CAST(DATE_TRUNC('day', t2.gclid_date) AS DATE) AS gclid_date,
t2.id AS id,
t2.medium AS medium,
CAST(DATE_TRUNC('day', t2.medium_date) AS DATE) AS medium_date,
t2.referrer_url AS referrer_url,
CAST(DATE_TRUNC('day', t2.referrer_url_date) AS DATE) AS referrer_url_date,
t2.source AS source,
CAST(DATE_TRUNC('day', t2.source_date) AS DATE) AS source_date,
t2.term AS term,
CAST(DATE_TRUNC('day', t2.term_date) AS DATE) AS term_date,
t2.user_id AS user_id
FROM "postgres"."public"."users" AS t1
LEFT JOIN LATERAL (
SELECT
*
FROM "postgres"."public"."signup_analytics" AS t2
WHERE
t1.id = t2.user_id
ORDER BY
t2.created_at DESC
LIMIT 1
) AS t2
ON TRUE
ORDER BY
t1.created_at DESC
LIMIT 500
By implementing this in your own system and database, you can enjoy several benefits:
The solution above is meant to be a simple and reliable way to start tracking. It's essential to be aware of its limitations:
gclid
(Google Click Identifier) expirations. It's important to consider these assumptions and adjust them accordingly to ensure accurate data analysis.In addition to storing signup referral data in your own database, another reliable way to get sign up referral tracking is by using a Product analytics tool (PostHog, Amplitude) with a proxy.
This means that the browser event will be sent to a proxy like a Cloudflare function hosted at proxy.yourdomain.com, then that Cloudflare function sends the event to your analytics tool.
Since the browser sees that you’re just talking to yourdomain.com, it is more likely to bypass ad blockers.
See Segment’s guide on how they allow you to track data via a custom proxy: https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/custom-proxy/
The fully first party solution described above will still be more reliable though. And it’s also easier for me to analyze it against other product metrics in the database. I often implement both.
Get the new standard in analytics. Sign up below or get in touch and we’ll set you up in under 30 minutes.