diff --git a/docs-src/src/components/trigger-event.tsx b/docs-src/src/components/trigger-event.tsx index 9817256a232..49f1445a71f 100644 --- a/docs-src/src/components/trigger-event.tsx +++ b/docs-src/src/components/trigger-event.tsx @@ -11,6 +11,101 @@ export type RedditEventType = | 'Lead' | 'Purchase'; +const CONVERSION_WORKER_URL = 'https://rxdb-events.daniel-meyer-e90.workers.dev'; +/** + * Written by storeAdClickId() in Root.tsx when the user lands with a + * gclid/gbraid/wbraid URL param. Shape: { k, v, t }. + */ +export const AD_CLICK_STORAGE_ID = 'click_id'; + +function getStoredAdClickId(): { k: string; v: string; t: number; } | null { + try { + const raw = localStorage.getItem(AD_CLICK_STORAGE_ID); + if (!raw) { + return null; + } + const parsed = JSON.parse(raw); + if (!parsed || !parsed.v) { + return null; + } + return parsed; + } catch (err) { + return null; + } +} + +/** + * Self-minted stable GA4-style client id. Only used when no _ga cookie + * exists, so the worker can forward events to the GA4 Measurement Protocol. + */ +function getOrMintClientId(): string { + let cid = localStorage.getItem('worker_cid'); + if (!cid) { + cid = Math.floor(Math.random() * 1e10) + '.' + Math.floor(Date.now() / 1000); + localStorage.setItem('worker_cid', cid); + } + return cid; +} + +function getSessionId(): string { + try { + let sid = sessionStorage.getItem('worker_sid'); + if (!sid) { + sid = Math.floor(Date.now() / 1000) + ''; + sessionStorage.setItem('worker_sid', sid); + } + return sid; + } catch (err) { + return Math.floor(Date.now() / 1000) + ''; + } +} + +function postToWorker(path: string, payload: any) { + const body = JSON.stringify(payload); + const url = CONVERSION_WORKER_URL + path; + if (navigator.sendBeacon) { + navigator.sendBeacon(url, body); + } else { + fetch(url, { method: 'POST', body, keepalive: true }).catch(() => { }); + } +} + +/** + * Sends every tracking event to the conversion worker: + * - to /api/google-ads when an ad click id is stored, so Google Ads can + * import the event as an offline conversion (which event names count, and + * whether they are primary or secondary, is decided in Google Ads by which + * conversion actions exist), + * - to /api/google-analytics when no google-analytics cookie exists (gtag + * blocked or never loaded), so the worker forwards the event to the GA4 + * Measurement Protocol. Users with a _ga cookie already report via gtag; + * skipping them here prevents double counting. + */ +function sendToConversionWorker(type: string, value: number) { + try { + const adClick = getStoredAdClickId(); + if (adClick) { + postToWorker('/api/google-ads', { + type, + value, + clid: adClick.v, + clidKind: adClick.k + }); + } + if (!document.cookie.includes('_ga=')) { + postToWorker('/api/google-analytics', { + type, + value, + cid: getOrMintClientId(), + sid: getSessionId() + }); + } + } catch (err) { + console.log('# Error on conversion-worker trigger:'); + console.dir(err); + } +} + export function triggerTrackingEvent( type: string, value: number, @@ -40,6 +135,12 @@ export function triggerTrackingEvent( console.log('triggerTrackingEvent(' + type + ', ' + value + ', redditEventType=' + redditEventType + ' ' + triggeredBefore + '/' + maxPerUser + ')'); + /** + * Google Ads conversion worker (runs after the same frequency capping + * as the other trackers). + */ + sendToConversionWorker(type, value); + /** * Reddit does not have a concept of conversion-value * so we only track primary events because otherwise everything would diff --git a/docs-src/src/theme/Root.tsx b/docs-src/src/theme/Root.tsx index 233cd06b23a..54ad69df242 100644 --- a/docs-src/src/theme/Root.tsx +++ b/docs-src/src/theme/Root.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { onCopy, triggerTrackingEvent } from '../components/trigger-event'; +import { AD_CLICK_STORAGE_ID, onCopy, triggerTrackingEvent } from '../components/trigger-event'; import { randomNumber } from '../../../plugins/utils'; import { IconClose } from '../components/icons/close'; import { Button } from '../components/button'; @@ -317,7 +317,7 @@ function storeAdClickId() { for (const k of ['gclid', 'gbraid', 'wbraid']) { const v = p.get(k); if (v) { - localStorage.setItem('click_id', JSON.stringify({ k, v, t: Date.now() })); + localStorage.setItem(AD_CLICK_STORAGE_ID, JSON.stringify({ k, v, t: Date.now() })); triggerTrackingEvent('click_id_' + k, 0.01, 1); } }