GA4 event name (e.g. "render_error").
Non-PII event parameters.
A promise that resolves once the send settles.
await trackEvent("render_error", { app_version: "1.6.1" });
export async function trackEvent(
name: string,
params: Record<string, string | number> = {},
): Promise<void> {
const { measurementId } = analyticsConfig;
if (!measurementId) return;
if (!(await analyticsEnabled())) return;
try {
const query = new URLSearchParams({
v: '2', // GA4 protocol version
tid: measurementId, // Measurement ID
cid: await getClientId(), // stable client id
en: name, // event name
_et: '1', // engagement time (ms)
});
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'number') query.set(`epn.${key}`, String(value));
else query.set(`ep.${key}`, value.slice(0, PARAM_VALUE_LIMIT));
}
// no-cors: an extension page may POST cross-origin without a host permission;
// keepalive lets it complete even if the page is tearing down after a crash.
await fetch(`${GA_ENDPOINT}?${query.toString()}`, {
method: 'POST',
mode: 'no-cors',
keepalive: true,
});
} catch {
// Best-effort telemetry: swallow all failures.
}
}
Sends one GA4 event to the
/g/collectendpoint. No-op (and no network call) until a measurement id is configured inconfig.json(analytics). Text params go through asep.<key>(truncated to GA4's limit), numeric params asepn.<key>. Never throws.