Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
/**
|
|
* Client analytics: GTM bootstrap, Consent Mode v2, dataLayer helpers.
|
|
*
|
|
* Setup (ops) — also documented on PUBLIC_GTM_ID in root `.env.example`:
|
|
* 1. Create a GA4 property in Google Analytics.
|
|
* 2. Create a GTM web container; set PUBLIC_GTM_ID=GTM-XXXX in root `.env`.
|
|
* 3. In GTM: GA4 Configuration tag with Consent Settings requiring
|
|
* analytics_storage (and ad_* for ads tags); publish the container.
|
|
* 4. SPA page views: Custom Event trigger `page_view` (from afterNavigate).
|
|
* Do NOT also enable GTM History Change / enhanced measurement page_view —
|
|
* that would double-count.
|
|
*
|
|
* Primary loading is GTM only — do not hard-code a GA measurement ID here.
|
|
*/
|
|
|
|
import { browser } from "$app/environment";
|
|
import { env } from "$env/dynamic/public";
|
|
import {
|
|
CONSENT_STORAGE_KEY,
|
|
CONSENT_WAIT_FOR_UPDATE_MS,
|
|
DENIED_CONSENT_DEFAULTS,
|
|
parseStoredConsent,
|
|
preferencesToConsentSignals,
|
|
type ConsentModeSignals,
|
|
type ConsentPreferences
|
|
} from "./analytics/consent-mode";
|
|
import { resolveGtmId } from "./analytics/gtm-id";
|
|
import {
|
|
toEcommerceObject,
|
|
type Ga4EcommerceFields
|
|
} from "./analytics/ecommerce";
|
|
|
|
export type { ConsentModeSignals, ConsentPreferences };
|
|
export type { Ga4EcommerceFields, Ga4EcommerceItem, Ga4ItemCategory } from "./analytics/ecommerce";
|
|
export {
|
|
CONSENT_STORAGE_KEY,
|
|
CONSENT_VERSION,
|
|
acceptAllPreferences,
|
|
estimatedSkusBucket,
|
|
parseStoredConsent,
|
|
preferencesToConsentSignals,
|
|
rejectNonEssentialPreferences,
|
|
serializeConsent
|
|
} from "./analytics/consent-mode";
|
|
export { resolveGtmId } from "./analytics/gtm-id";
|
|
export {
|
|
buildCreditPackEcommerce,
|
|
buildSubscriptionEcommerce,
|
|
claimPurchaseTracking,
|
|
resolveCheckoutEcommerceFromParams,
|
|
safeCheckoutSessionId
|
|
} from "./analytics/ecommerce";
|
|
|
|
type DataLayer = Array<Record<string, unknown> | IArguments | unknown[]>;
|
|
|
|
declare global {
|
|
interface Window {
|
|
dataLayer?: DataLayer;
|
|
gtag?: (...args: unknown[]) => void;
|
|
__descrybeGtmLoaded?: string;
|
|
__descrybeConsentDefaulted?: boolean;
|
|
}
|
|
}
|
|
|
|
function ensureDataLayer(): DataLayer {
|
|
if (!browser) return [];
|
|
window.dataLayer = window.dataLayer ?? [];
|
|
return window.dataLayer;
|
|
}
|
|
|
|
function ensureGtag(): void {
|
|
if (!browser) return;
|
|
ensureDataLayer();
|
|
if (typeof window.gtag === "function") return;
|
|
window.gtag = function gtag(...args: unknown[]) {
|
|
ensureDataLayer().push(args);
|
|
};
|
|
}
|
|
|
|
/** True when the user granted analytics_storage via the CMP. */
|
|
export function isAnalyticsGranted(): boolean {
|
|
if (!browser) return false;
|
|
try {
|
|
const stored = parseStoredConsent(localStorage.getItem(CONSENT_STORAGE_KEY));
|
|
return stored?.analytics === true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Call before GTM injects — EU/EEA default denied until CMP update. */
|
|
export function ensureConsentDefaults(): void {
|
|
if (!browser || window.__descrybeConsentDefaulted) return;
|
|
ensureGtag();
|
|
window.gtag?.("consent", "default", {
|
|
...DENIED_CONSENT_DEFAULTS,
|
|
wait_for_update: CONSENT_WAIT_FOR_UPDATE_MS
|
|
});
|
|
window.__descrybeConsentDefaulted = true;
|
|
}
|
|
|
|
export function updateConsentMode(prefs: ConsentPreferences): void {
|
|
if (!browser) return;
|
|
ensureGtag();
|
|
ensureConsentDefaults();
|
|
const signals = preferencesToConsentSignals(prefs);
|
|
window.gtag?.("consent", "update", signals);
|
|
pushDataLayer({
|
|
event: "consent_update",
|
|
analytics_storage: signals.analytics_storage,
|
|
ad_storage: signals.ad_storage,
|
|
ad_user_data: signals.ad_user_data,
|
|
ad_personalization: signals.ad_personalization
|
|
});
|
|
}
|
|
|
|
export function pushDataLayer(payload: Record<string, unknown>): void {
|
|
if (!browser) return;
|
|
ensureDataLayer().push(payload);
|
|
}
|
|
|
|
/**
|
|
* Custom event helper. No-ops until analytics_storage is granted via CMP.
|
|
* Never pass email, password, tokens, names, phone, SKU/GTIN/title, or API keys.
|
|
*/
|
|
export function trackEvent(event: string, params?: Record<string, unknown>): void {
|
|
if (!browser) return;
|
|
if (!isAnalyticsGranted()) return;
|
|
const name = event.trim();
|
|
if (!name) return;
|
|
pushDataLayer({
|
|
event: name,
|
|
...(params ?? {})
|
|
});
|
|
}
|
|
|
|
/**
|
|
* GA4 ecommerce custom event (GTM). Clears prior ecommerce, then pushes
|
|
* `{ event, ecommerce: { currency?, value?, transaction_id?, items } }`.
|
|
* Consent-gated like trackEvent. Never pass PII (email, cus_*, names).
|
|
*/
|
|
export function trackEcommerceEvent(
|
|
event: string,
|
|
ecommerce: Ga4EcommerceFields,
|
|
extra?: Record<string, unknown>
|
|
): void {
|
|
if (!browser) return;
|
|
if (!isAnalyticsGranted()) return;
|
|
const name = event.trim();
|
|
if (!name) return;
|
|
pushDataLayer({ ecommerce: null });
|
|
pushDataLayer({
|
|
event: name,
|
|
ecommerce: toEcommerceObject(ecommerce),
|
|
...(extra ?? {})
|
|
});
|
|
}
|
|
|
|
/**
|
|
* SPA page_view for GTM (Custom Event trigger: page_view).
|
|
* Gated on analytics consent. Prefer this over GTM History Change.
|
|
*/
|
|
export function trackPageview(path: string, opts?: { title?: string; location?: string }): void {
|
|
if (!browser) return;
|
|
if (!isAnalyticsGranted()) return;
|
|
pushDataLayer({
|
|
event: "page_view",
|
|
page_path: path,
|
|
page_title: opts?.title ?? document.title,
|
|
page_location: opts?.location ?? window.location.href
|
|
});
|
|
}
|
|
|
|
export function resolveConfiguredGtmId(): string | null {
|
|
return resolveGtmId(env.PUBLIC_GTM_ID);
|
|
}
|
|
|
|
/** Load GTM container once when PUBLIC_GTM_ID is a valid GTM-XXXX id. */
|
|
export function loadGoogleTagManager(gtmId = resolveConfiguredGtmId()): string | null {
|
|
if (!browser) return null;
|
|
const id = resolveGtmId(gtmId);
|
|
if (!id) return null;
|
|
if (window.__descrybeGtmLoaded === id) return id;
|
|
|
|
ensureConsentDefaults();
|
|
ensureDataLayer().push({ "gtm.start": Date.now(), event: "gtm.js" });
|
|
|
|
const script = document.createElement("script");
|
|
script.async = true;
|
|
script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(id)}`;
|
|
document.head.appendChild(script);
|
|
|
|
window.__descrybeGtmLoaded = id;
|
|
return id;
|
|
}
|