chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# nx-dev-feature-analytics
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test nx-dev-feature-analytics` to execute the unit tests via [Jest](https://jestjs.io).
@@ -0,0 +1,4 @@
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
import nx from '@nx/eslint-plugin';
export default [...baseConfig, ...nx.configs['flat/react'], ...reactHooksV7Off];
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
displayName: 'nx-dev-feature-analytics',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/feature-analytics',
preset: '../../jest.preset.js',
};
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@nx/nx-dev-feature-analytics",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "nx-dev-feature-analytics",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/feature-analytics/src",
"projectType": "library",
"targets": {},
"tags": ["scope:nx-dev", "type:feature"]
}
+2
View File
@@ -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 ?? {}),
});
}
+123
View File
@@ -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]);
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,21 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["node"],
"composite": true,
"declaration": true
},
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"jest.config.ts"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/spec",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}