chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export * from './lib/google-analytics';
|
||||
export * from './lib/use-window-scroll-depth';
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* GA4 events are routed through GTM via dataLayer (no direct gtag calls here).
|
||||
*/
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer?: Array<Record<string, unknown>>;
|
||||
}
|
||||
}
|
||||
|
||||
function pushGtmEvent(eventName: string, payload: Record<string, unknown>) {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({ event: eventName, ...payload });
|
||||
}
|
||||
|
||||
export function sendPageViewEventViaGtm(data: {
|
||||
path?: string;
|
||||
title?: string;
|
||||
}): void {
|
||||
pushGtmEvent('page_view', {
|
||||
...(data.path ? { page_path: data.path } : {}),
|
||||
...(data.title ? { page_title: data.title } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function sendCustomEventViaGtm(
|
||||
action: string,
|
||||
category: string,
|
||||
label: string,
|
||||
value?: number,
|
||||
customObject?: Record<string, unknown>
|
||||
): void {
|
||||
// Preserve existing GA event names by using the action as the dataLayer event.
|
||||
pushGtmEvent(action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
...(value !== undefined ? { value } : {}),
|
||||
...(customObject ?? {}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
export interface Gtag {
|
||||
(
|
||||
command: 'config',
|
||||
targetId: string,
|
||||
config?: ConfigParams | ControlParams | EventParams | CustomParams
|
||||
): void;
|
||||
(command: 'set', targetId: string, config: CustomParams | boolean): void;
|
||||
(command: 'set', config: CustomParams): void;
|
||||
(command: 'js', config: Date): void;
|
||||
(
|
||||
command: 'event',
|
||||
eventName: EventNames | string,
|
||||
eventParams?: ControlParams | EventParams | CustomParams
|
||||
): void;
|
||||
(
|
||||
command: 'get',
|
||||
targetId: string,
|
||||
fieldName: FieldNames | string,
|
||||
callback?: (field: string) => any
|
||||
): void;
|
||||
(
|
||||
command: 'consent',
|
||||
consentArg: ConsentArg | string,
|
||||
consentParams: ConsentParams
|
||||
): void;
|
||||
}
|
||||
|
||||
interface CustomParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface ConfigParams {
|
||||
page_location?: string;
|
||||
page_path?: string;
|
||||
page_title?: string;
|
||||
send_page_view?: boolean;
|
||||
}
|
||||
|
||||
interface ControlParams {
|
||||
groups?: string | string[];
|
||||
send_to?: string | string[];
|
||||
event_callback?: () => void;
|
||||
event_timeout?: number;
|
||||
}
|
||||
|
||||
type EventNames =
|
||||
| 'add_payment_info'
|
||||
| 'add_to_cart'
|
||||
| 'add_to_wishlist'
|
||||
| 'begin_checkout'
|
||||
| 'checkout_progress'
|
||||
| 'exception'
|
||||
| 'generate_lead'
|
||||
| 'login'
|
||||
| 'page_view'
|
||||
| 'purchase'
|
||||
| 'refund'
|
||||
| 'remove_from_cart'
|
||||
| 'screen_view'
|
||||
| 'search'
|
||||
| 'select_content'
|
||||
| 'set_checkout_option'
|
||||
| 'share'
|
||||
| 'sign_up'
|
||||
| 'timing_complete'
|
||||
| 'view_item'
|
||||
| 'view_item_list'
|
||||
| 'view_promotion'
|
||||
| 'view_search_results';
|
||||
|
||||
interface EventParams {
|
||||
checkout_option?: string;
|
||||
checkout_step?: number;
|
||||
content_id?: string;
|
||||
content_type?: string;
|
||||
coupon?: string;
|
||||
currency?: string;
|
||||
description?: string;
|
||||
fatal?: boolean;
|
||||
items?: Item[];
|
||||
method?: string;
|
||||
number?: string;
|
||||
promotions?: Promotion[];
|
||||
screen_name?: string;
|
||||
search_term?: string;
|
||||
shipping?: Currency;
|
||||
tax?: Currency;
|
||||
transaction_id?: string;
|
||||
value?: number;
|
||||
event_label?: string;
|
||||
event_category?: string;
|
||||
}
|
||||
|
||||
type Currency = string | number;
|
||||
|
||||
interface Item {
|
||||
brand?: string;
|
||||
category?: string;
|
||||
creative_name?: string;
|
||||
creative_slot?: string;
|
||||
id?: string;
|
||||
location_id?: string;
|
||||
name?: string;
|
||||
price?: Currency;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
interface Promotion {
|
||||
creative_name?: string;
|
||||
creative_slot?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type FieldNames = 'client_id' | 'session_id' | 'gclid';
|
||||
|
||||
type ConsentArg = 'default' | 'update';
|
||||
interface ConsentParams {
|
||||
ad_storage?: 'granted' | 'denied';
|
||||
analytics_storage?: 'granted' | 'denied';
|
||||
wait_for_update?: number;
|
||||
region?: string[];
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { sendCustomEventViaGtm } from './google-analytics';
|
||||
|
||||
const SCROLL_THRESHOLDS = [10, 25, 50, 75, 90] as const;
|
||||
|
||||
export function useWindowScrollDepth(): void {
|
||||
const pathname = usePathname();
|
||||
const firedThresholds = useRef<Set<number>>(new Set());
|
||||
const shouldTrackScroll = useRef(true);
|
||||
const rafId = useRef<number | null>(null);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!shouldTrackScroll.current) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const scrollPercentage =
|
||||
((window.scrollY + window.innerHeight) /
|
||||
document.documentElement.scrollHeight) *
|
||||
100;
|
||||
|
||||
// Fire events for all thresholds we've passed but haven't fired yet
|
||||
for (const threshold of SCROLL_THRESHOLDS) {
|
||||
if (
|
||||
scrollPercentage >= threshold &&
|
||||
!firedThresholds.current.has(threshold)
|
||||
) {
|
||||
firedThresholds.current.add(threshold);
|
||||
sendCustomEventViaGtm(`scroll_${threshold}`, 'scroll', pathname || '/');
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
const throttledHandleScroll = useCallback(() => {
|
||||
if (rafId.current !== null) return;
|
||||
|
||||
rafId.current = requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
rafId.current = null;
|
||||
});
|
||||
}, [handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
shouldTrackScroll.current = false;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
firedThresholds.current = new Set();
|
||||
shouldTrackScroll.current = true;
|
||||
// Immediately check current scroll position to capture thresholds
|
||||
// that may have been passed during the delay
|
||||
handleScroll();
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [pathname, handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.addEventListener('scroll', throttledHandleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledHandleScroll);
|
||||
if (rafId.current !== null) {
|
||||
cancelAnimationFrame(rafId.current);
|
||||
}
|
||||
};
|
||||
}, [throttledHandleScroll]);
|
||||
}
|
||||
Reference in New Issue
Block a user