Last active
July 28, 2022 01:55
-
-
Save rawmain/0c0635c6ad0fb7e15c997a42fb509d54 to your computer and use it in GitHub Desktop.
GDPR-compliant GA4 single-host proxy implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
include_once('gaproxy4.class.php'); | |
$ga4 = new GaProxy4(); | |
$ga4->sendHit4(); | |
?> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); | |
} | |
} | |
?> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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