Skip to content

Instantly share code, notes, and snippets.

@rawmain
Last active July 28, 2022 01:55
Show Gist options
  • Save rawmain/0c0635c6ad0fb7e15c997a42fb509d54 to your computer and use it in GitHub Desktop.
Save rawmain/0c0635c6ad0fb7e15c997a42fb509d54 to your computer and use it in GitHub Desktop.
GDPR-compliant GA4 single-host proxy implementation
<?php
include_once('gaproxy4.class.php');
$ga4 = new GaProxy4();
$ga4->sendHit4();
?>
<?php
class GaProxy4 {
// Configuration Start
// Set your real Property Nmber where you want to redirect the data
private $property_id = 'G-XXXXXXXXXX'; // replace with your GA4 measurement-ID
// This will be attached to the hostname value, so we can then filter any hit not coming from this script
private $filterHash = 'GDPR-TEST-GA4-';
// set this to true, if you want to remove the last Ip's Octet
private $anonymizeIp2 = true;
// Configuration End
public $payload4;
// We are setting the jail for the visitors, a PHP is started and the session_id is saved into a session variable
// When collect.php is loaded we will check the current session_id against this value, and if it doesn't match we'll abort sending the hit to Google Analytics
function setupProxy4()
{
session_start();
$_SESSION["ga4jail_session_id"] = session_id();
$_SESSION["ga4jail_current_url"] = $_SERVER['REQUEST_URI'];
}
// We'll build the hit on there, adding the current user IP address to keep the Geolocation reports, and the user agent
// Again, we'll setup out real filtered UA property in here. An attacker will not be able to guess our real UA account.
function setupHit4()
{
$this->payload4 = $_GET;
$this->payload4["_uip"] = $this->getIpAddress();
$this->payload4["ua"] = $this->getUserAgent();
$this->payload4["tid"] = $this->property_id;
$this->payload4["dh"] = $this->filterHash.'-'.$this->getRequestHostName();
}
// This will be used to add more antispam mechanism in a future.
function checkRequestHeaders()
{
// TODO
// Check User Agent Format : bots, malformed user agents, etc
// Check Againts spammers blacklist IP's
// Check throttling
}
// Gets the current loading user agent
function getUserAgent()
{
$user_agent = $_SERVER["HTTP_USER_AGENT"];
if (strpos($user_agent, 'Opera') || strpos($user_agent, 'OPR/')) return 'Opera';
elseif (strpos($user_agent, 'Edg/')
|| strpos($user_agent, 'EdgA/')
|| strpos($user_agent, 'EdgiOS/')
|| strpos($user_agent, 'Edge/')) return 'Edge';
elseif (strpos($user_agent, 'Chrome')) return 'Chrome';
elseif (strpos($user_agent, 'Safari')) return 'Safari';
elseif (strpos($user_agent, 'Firefox')) return 'Firefox';
elseif (strpos($user_agent, 'MSIE') || strpos($user_agent, 'Trident')) return 'Internet Explorer';
return 'Other';
}
// Gets the current hostnamea
function getRequestHostName()
{
return $_SERVER["SERVER_NAME"];
}
// Gets the current loading Ip Address
function getIpAddress()
{
$ipAddress = $_SERVER['REMOTE_ADDR']?:($_SERVER['HTTP_X_FORWARDED_FOR']?:$_SERVER['HTTP_CLIENT_IP']);
if (strpos($ipAddress, ':'))
{
$replace_num = strrpos($ipAddress, ':') - strlen($ipAddress) + 1;
$ipAddress6 = substr_replace($ipAddress, '0000', $replace_num);
return $ipAddress6;
}
elseif (strpos($ipAddress, '.'))
{
$octets = explode('.', $ipAddress);
$second_to_last = array_slice($octets, -2, 1, true);
$second_to_last_key = array_key_first($second_to_last);
$second_to_last[$second_to_last_key] = '0';
$last = array_slice($octets, -1, 1, true);
$last_key = array_key_first($last);
$last[$last_key] = '0';
$octets = array_replace($octets, $second_to_last, $last);
return implode('.', $octets);
}
return '185.27.0.0'; // replace with the anonymized IP Address of your server
}
// This function will care of building the hit payload and sending it to Universal Analytics endpoint
function sendHit4()
{
session_start();
$this->setupHit4();
if($_SESSION["ga4jail_session_id"] == session_id())
{
$context = stream_context_create(array('http' => array(
'method' => 'GET',
'header'=>"Accept-language: ".$this->payload4["ul"]."\r\n" .
"User-Agent: ".$this->payload4["ua"]."\r\n"
)));
$url = 'https://region1.google-analytics.com/g/collect?'.http_build_query($this->payload4,'','&');
$fp = fopen($url, 'r', false, $context);
}
// Used for debug
// else
//{
// die("ABORTING HIT");
//}
// Return a 1x1 Gif Pixel
// Not sure if creating it using base64 is the fastest way, it may need improvement
header('Content-Type: image/gif');
echo base64_decode("R0lGODdhAQABAIAAAPxqbAAAACwAAAAAAQABAAACAkQBADs=");
die();
}
}
?>
<script src='https://YOURWEBSITE/mazinger4.js'></script>
// Include it in every webpage you want to collect pageview hits
// P.S.: Replace YOURWEBSITE with your own site address
(function (context, trackingId, options) {
const history = context.history;
const doc = document;
const nav = navigator || {};
const storage = localStorage;
const sStor = sessionStorage;
const encode = encodeURIComponent;
const pushState = history.pushState;
const typeException = 'exception';
const enScroll = false;
const generateZ = () => Math.floor(1000000000 + (Math.random() * 8999999999)).toString();
if (!Math.imul) Math.imul = function(opA, opB) {
opB |= 0;
var result = (opA & 0x003fffff) * opB;
if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB|0;
return result|0;
};
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
const _pId = () => {
if (!sStor._p) {
sStor._p = generateZ();
}
return sStor._p;
};
const cidCheck = storage.getItem("CID_HASHED");
const _fvId = () => {
if(cidCheck) {
return undefined;
}
else if(enScroll==true) {
return undefined;
}
else {
return "1";
}
};
const dategenId = () => Math.floor(Date.now() / 1000).toString();
const sidId = () => {
if (!sStor.sid) {
sStor.sid = dategenId();
}
return sStor.sid;
};
const _ssId = () => {
if (!sStor._ss) {
sStor._ss = "1";
return sStor._ss;
}
else if(sStor.getItem("_ss") == "1") {
return undefined;
}
};
const generatesctId = "1";
const sctId = () => {
if (!sStor.sct) {
sStor.sct = generatesctId;
}
else if(enScroll==true) {
return sStor.sct;
}
else {
x = +sStor.getItem("sct") + +generatesctId;
sStor.sct = x;
}
return sStor.sct;
};
const generateId = () => {
let creationTime = new Date().getTime();
let expiryTime = creationTime + (30 * 24 * 3600 * 1000); // Change 30 to any number of days you want the CID to be valid.
let CIDSource = window.location.host + ";" + navigator.userAgent + ";" + navigator.language + ";" + creationTime;
if (window.localStorage) {
CIDhashed = localStorage.getItem('CID_HASHED');
CIDexpiry = localStorage.getItem('CID_EXPIRY');
if ((CIDhashed === null || CIDexpiry === null)
|| (CIDhashed !== null && CIDexpiry !== null && CIDexpiry >= expiryTime)) {
localStorage.setItem('CID_HASHED', cyrb53(CIDSource).toString(16));
localStorage.setItem('CID_EXPIRY', expiryTime);
}
return storage.CID_HASHED;
} else {
return undefined;
}
};
const getId = () => {
if (!storage.CID_HASHED) {
storage.CID_HASHED = generateId();
}
return storage.CID_HASHED;
};
const serialize = (obj) => {
var str = [];
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
if(obj[p] !== undefined) {
str.push(encode(p) + "=" + encode(obj[p]));
}
}
}
return str.join("&");
};
const track = (
type,
eventCategory,
eventAction,
eventLabel,
eventValue,
exceptionDescription,
exceptionFatal
) => {
const url4 = '/collect4.php';
const data4 = serialize({
v: '2',
ds: undefined,
aip: options.anonymizeIp ? 1 : undefined,
tid: trackingId,
cid: getId(),
en: type || 'page_view',
sd: options.colorDepth && screen.colorDepth ? `${screen.colorDepth}-bits` : undefined,
dr: undefined,
dt: doc.title,
dl: doc.location.origin + doc.location.pathname,
ul: options.language ? (nav.language || "").toLowerCase() : undefined,
de: options.characterSet ? doc.characterSet : undefined,
sr: options.screenSize ? `${(context.screen || {}).width}x${(context.screen || {}).height}` : undefined,
vp: options.screenSize && context.visualViewport ? `${(context.visualViewport || {}).width}x${(context.visualViewport || {}).height}` : undefined,
ec: eventCategory || undefined,
ea: eventAction || undefined,
el: eventLabel || undefined,
ev: eventValue || undefined,
exd: exceptionDescription || undefined,
exf: typeof exceptionFatal !== 'undefined' && !!exceptionFatal === false ? 0 : undefined,
_z: generateZ(),
_p: _pId(),
_fv: _fvId(), // first_visit, identify returning users based on existance of client ID in localStorage
_s: "1", // session hits counter
sid: sidId(), // session ID random generated, hold in sessionStorage
sct: sctId(), // session count for a user, increase +1 in new interaction
seg: "1", // session engaged (interacted for at least 10 seconds), assume yes
_ss: _ssId(), // session_start, new session start
});
var xhr = new XMLHttpRequest();
xhr.open("GET", url4+"?"+data4);
xhr.send();
};
const trackEvent = (category, action, label, value) => track('event', category, action, label, value);
const trackException = (description, fatal) => track(typeException, null, null, null, null, description, fatal);
history.pushState = function (state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({ state: state });
}
setTimeout(track, options.delay || 10);
return pushState.apply(history, arguments);
}
track();
context.ma = {
trackEvent,
trackException
}
})(window, "MAZINGER4", {
anonymizeIp: true,
colorDepth: true,
characterSet: true,
screenSize: true,
language: true
});
<?php
include_once('gaproxy4.class.php');
$ga4 = new GaProxy4();
$ga4->setupProxy4();
/**
* Include this code before HTML tag
* for every webpage you want to
* collect pageview hits
*/
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment