This code sends data direct to the GA measurements end-points
to avoid errors when running ga.js locally or with no DOM.
Trigger a custom GA event.
(you can add any fields you like to this)
import { sendCustomEvent } from "./ga-direct.mjs"
sendCustomEvent( app.GetDeviceId(), buttonName+'_touch', {
buttonName: buttonName,
screenName: screenName,
page_context: 'home'
})
*/
//Google Analytics config.
const DEBUG = false
const MEASUREMENT_ID = 'G-NXXXXXXJHW'
const API_SECRET = 'T-E-OFkXXXX_nzW8dp-Dg'
const CITY = 'Paris'
export function sendCustomEvent(clientId, name, params) {
sendEvent(clientId, name, {
session_id: getSessionId(),
session_engaged: 1,
engagement_time_msec: 100,
...params
});
}
export function sendPageView(clientId, title, virtualUrl) {
sendEvent(clientId, 'page_view', {
page_title: title,
page_location: virtualUrl,
page_referrer: document.referrer || 'none',
session_id: getSessionId(),
session_engaged: 1,
engagement_time_msec: 100
});
}
async function sendEvent(clientId, name, params = {}) {
const cleanedParams = {};
// Clean up values to keep GA happy.
for (const [key, value] of Object.entries(params)) {
const cleanKey = clean(key)
let cleanValue
if (typeof value === 'string') cleanValue = clean(value)
else cleanValue = value
if (cleanKey) cleanedParams[cleanKey] = cleanValue
}
const endpoint = `
https://www.google-analytics.com/mp/collect?` +
`measurement_id=${encodeURIComponent(MEASUREMENT_ID)}` +
`&api_secret=${encodeURIComponent(API_SECRET)}`;
const body = {
client_id: toStableGaClientId(clientId),
//user_location: { city: "Leeds", region_id: "GB-LDS", country_id: "GB" },
user_location: { city: CITY, region_id: 'GB-ENG', country_id: 'GB' },
//ip_override: '81.143.128.0',
events: [{ name, params: cleanedParams }]
};
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json', 'user-agent': 'Mozilla/5.0' },
body: JSON.stringify(body)
})
if (!res.ok) console.error('GA4 MP error:', res.status, await res.text());
else if( DEBUG ) console.log('GA4 MP event sent:', name, JSON.stringify(body,0,2) )
} catch (e) {
console.error('GA4 MP fetch failed:', e)
}
}
function toStableGaClientId(input) {
const s = String(input ?? '').trim()
// If it's already GA-ish, keep it
if (/^\d+\.\d+$/.test(s)) return s
// FNV-1a 64-bit hash (BigInt) - deterministic and fast
let h = 0xcbf29ce484222325n
for (let i = 0; i < s.length; i++) {
h ^= BigInt(s.charCodeAt(i))
h = (h * 0x100000001b3n) & 0xffffffffffffffffn
}
// Client "random" portion (keep it reasonably sized)
const clientPart = Number(h % 10000000000n) // 0..9,999,999,999
// Deterministic "timestamp" portion, but keep it plausible
// Map into 2019-01-01 .. 2030-01-01
const base = 1546300800n // 2019-01-01T00:00:00Z
const span = 347129280n // 11 years in seconds (approx; good enough)
const tsPart = Number(base + ((h >> 32n) % span))
return `${clientPart}.${tsPart}`
}
function getSessionId() {
if( !window._sid )
window._sid = Math.floor(Date.now()/1000)
return window._sid
}
function newSession() {
window._sid = Date.now().toString()
}
function activateUser() {
sendEvent('user_engagement', {
session_id: getSessionId(),
engagement_time_msec: 5000,
session_engaged: 1
})
}
function clean(s) {
if (!s) return '';
return s
.toLowerCase()
.replace(/[^a-z0-9]/g, '_')
.substring(0, 40);
}