Analytics
PostHog Setup for South African Products.
PostHog is the analytics and product suite South African SaaS and ecommerce teams reach for when GA4 runs out of depth. This guide covers picking EU Cloud (closest region to SA), installing the JS SDK with POPIA-aligned defaults, wiring Shopify checkout events, and running the first funnel. We also cover reverse-proxying through Cloudflare to beat ad blockers on Telkom ADSL.
Prerequisites
- A PostHog Cloud account — pick EU region for lower SA latency
- PostHog Project API Key (phc_...)
- Access to theme or HTML head to inject the SDK
- Optional: a Cloudflare account if you plan to reverse-proxy
Step 1. Pick EU Cloud over US Cloud
PostHog Cloud has US and EU regions. From Johannesburg, EU routes through Frankfurt or Amsterdam with sub-200ms latency. US routes hairpin via New York and frequently breach 280ms. Create your project in the EU region. Note this in your privacy policy as a cross-border transfer to the EU.
Step 2. Install the JS SDK
Paste the snippet into <head>. Replace the key. Set persistence to "memory" initially if POPIA consent has not been granted — events still queue locally and upload on consent.
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_YOUR_KEY', {
api_host: 'https://eu.i.posthog.com',
persistence: 'memory',
autocapture: false,
capture_pageview: false,
disable_session_recording: true
});
</script> Step 3. Reverse-proxy through Cloudflare Workers
Ad blockers (uBlock Origin, Brave Shields) block posthog.com hosts. Proxy through your own domain (e.g. events.yourstore.co.za) with a Cloudflare Worker. This adds ~5ms and unblocks ~15 percent of SA desktop traffic that uses blockers.
export default {
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);
const target = new URL('https://eu.i.posthog.com' + url.pathname + url.search);
const body = req.method !== 'GET' && req.method !== 'HEAD' ? await req.arrayBuffer() : undefined;
const upstream = await fetch(target.toString(), {
method: req.method,
headers: req.headers,
body,
});
return new Response(upstream.body, {
status: upstream.status,
headers: upstream.headers,
});
}
}; Step 4. Upgrade persistence after POPIA consent
When the user accepts analytics consent, flip persistence from "memory" to "localStorage+cookie" and flush queued events. PostHog respects the change at runtime with set_config.
document.addEventListener('popia:consent:accepted', (e) => {
if (!e.detail.categories.includes('analytics')) return;
posthog.set_config({
persistence: 'localStorage+cookie',
disable_session_recording: false,
capture_pageview: true,
autocapture: true
});
posthog.capture('$pageview');
}); Step 5. Capture Shopify purchase events
Use a Shopify Custom Pixel (Settings > Customer events) to capture checkout_completed into PostHog. Pass ZAR value and order ID. PostHog stores currency as a plain property, so add a currency field.
analytics.subscribe('checkout_completed', (event) => {
const c = event.data.checkout;
window.posthog?.capture('purchase', {
order_id: c.order.id,
value: Number(c.totalPrice.amount),
currency: c.totalPrice.currencyCode,
tax: Number(c.totalTax?.amount || 0),
item_count: c.lineItems.length,
payment_method: c.transactions?.[0]?.paymentMethod?.name
});
}); Step 6. Identify logged-in customers without leaking PII
On login or account page load, call posthog.identify with a stable hash of the customer email, not the raw email. This is a POPIA good-practice pattern — you keep cross-device stitching without replicating the email in a US/EU analytics silo.
async function sha256Hex(str) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str.trim().toLowerCase()));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}
if (window.customer?.email) {
sha256Hex(window.customer.email).then((id) => {
window.posthog?.identify(id, {
country: 'ZA',
has_account: true
});
});
} Step 7. Build a checkout funnel
In PostHog > Insights > New funnel, add product_view > add_to_cart > begin_checkout > purchase. Filter by Country equals "ZA" and Device equals "Mobile". Watch the drop-off at begin_checkout — SA mobile users often lose signal on the Payfast redirect and never return.
Step 8. Use feature flags to A/B test Payfast vs Yoco default
Create a feature flag "default_payment_method" with variants payfast (50%), yoco (50%). Check the flag on the cart page and reorder the payment method list. Compare purchase rate by variant in the Experiments tab.
posthog.onFeatureFlags(() => {
const variant = posthog.getFeatureFlag('default_payment_method');
if (variant === 'yoco') {
// Reorder DOM: Yoco first
document.querySelector('[data-payment="yoco"]')?.setAttribute('data-preferred', '1');
}
}); SA gotchas
- PostHog US Cloud adds 150ms+ to every event from SA. Always pick EU Cloud unless you have a compliance reason to stay in US.
- Autocapture plus session recording is chatty on mobile. For data-capped SA users, disable session recording except on a sampled 10 percent.
- Reverse-proxying via Cloudflare costs 10 million free requests per month. High-traffic stores hit that in week one — budget for Workers Paid.
- PostHog identify() stores the raw distinct_id in localStorage. Using the email hash instead of email protects against POPIA breach notifications.
- Feature flags evaluate client-side by default, so they leak logic. For pricing experiments, use server-side evaluation via the PostHog Node SDK.
Frequently asked questions
Is PostHog POPIA compliant?
PostHog provides a Data Processing Addendum and EU residency. Combined with consent gating and hashed identifiers, it meets POPIA requirements. Disclose EU cross-border transfer in your privacy policy.
PostHog Cloud or self-hosted for a SA startup?
Cloud. Self-hosted PostHog is a Kubernetes beast that needs a dedicated engineer. For under 1M events per month, EU Cloud free tier is enough.
Do I still need GA4 if I use PostHog?
For Google Ads and SEO reporting, yes. GA4 is the default Google ecosystem citizen. Run both and let PostHog own product analytics.
Why do my PostHog counts differ from Shopify Admin orders?
Usually consent blocks the purchase event for about 10 percent of users, or network errors during the checkout redirect. For revenue-critical counts, pair client events with a server-side webhook.