Skip to content

Commit 2baa712

Browse files
yangw-devclee2000
andauthored
Add event tracking use GA (#6940)
# Desription Add google analytics utils to track UI usage. By default, each page will have a sessionId to track the user interaction flow for targeting UI components (such as click button, scroll etc). If user navigate to another page in a new tab, Or close the page and reopen, it will create a new session id, This pr provides two ways to target ui components: 1. add method 2. add attribute Please see the pr for more details. --------- Signed-off-by: Yang Wang <[email protected]> Co-authored-by: clee2000 <[email protected]>
1 parent 3380b47 commit 2baa712

File tree

11 files changed

+341
-26
lines changed

11 files changed

+341
-26
lines changed

torchci/components/commit/WorkflowBox.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ function WorkflowJobSummary({
106106
<JobButton
107107
variant="outlined"
108108
href={`/utilization/${m.workflow_id}/${m.job_id}/${m.run_attempt}`}
109+
data-ga-action="utilization_report_click"
110+
data-ga-label="nav_button"
111+
data-ga-category="user_interaction"
112+
data-ga-event-types="click"
109113
>
110114
Utilization Report{" "}
111115
</JobButton>

torchci/components/queueTimeAnalysis/components/charts/QueueTimeEchartElement.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,6 @@ const getPercentileLineChart = (
314314
const lines = [];
315315
const date = params[0].axisValue;
316316
lines.push(`<b>${date}</b>`);
317-
console.log(lines);
318317
for (const item of params) {
319318
const idx = item.data;
320319
const lineName = item.seriesName;

torchci/components/queueTimeAnalysis/components/searchBarItems/QueueTimeSearchBar.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { propsReducer } from "components/benchmark/llms/context/BenchmarkProps";
55
import { DateRangePicker } from "components/queueTimeAnalysis/components/pickers/DateRangePicker";
66
import { TimeGranuityPicker } from "components/queueTimeAnalysis/components/pickers/TimeGranuityPicker";
77
import dayjs from "dayjs";
8+
import { trackEventWithContext } from "lib/tracking/track";
89
import { cloneDeep } from "lodash";
910
import { NextRouter } from "next/router";
1011
import { ParsedUrlQuery } from "querystring";
@@ -232,6 +233,9 @@ export default function QueueTimeSearchBar({
232233

233234
const onSearch = () => {
234235
const newprops = cloneDeep(props);
236+
trackEventWithContext("qta_search", "user_interaction", "button_click", {
237+
data: newprops.category,
238+
});
235239
updateSearch({ type: "UPDATE_FIELDS", payload: newprops });
236240
};
237241

@@ -330,7 +334,15 @@ export default function QueueTimeSearchBar({
330334
</ScrollBar>
331335
<SearchButton>
332336
<Box sx={{ borderBottom: "1px solid #eee", padding: "0 0" }} />
333-
<RainbowButton onClick={onSearch}>Search</RainbowButton>
337+
<RainbowButton
338+
data-ga-action="qta_search_click"
339+
data-ga-label="search_button"
340+
data-ga-category="cta"
341+
data-ga-event-types="click"
342+
onClick={onSearch}
343+
>
344+
Search
345+
</RainbowButton>
334346
<FormHelperText>
335347
<span style={{ color: "red" }}>*</span> Click to apply filter
336348
changes
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Event Tracking guidance
2+
3+
# Overview
4+
5+
Guidance for event tracking in torchci.
6+
7+
## Google Analytics Development Guide (Local Development)
8+
9+
### Overview
10+
11+
This guide explains how to enable Google Analytics 4 (GA4) debug mode during local development so you can verify event tracking in real time via GA DebugView.
12+
13+
### Prerequisites
14+
15+
- TorchCI front-end development environment is set up and running locally
16+
- Chrome browser installed
17+
- Install chrome extension [Google Analytics Debugger](https://chrome.google.com/webstore/detail/jnkmfdileelhofjcijamephohjechhna)
18+
- Make sure you have permission to the GCP project `pytorch-hud` as admin. If not, reach out to `oss support (internal only)` or @pytorch/pytorch-dev-infra to add you
19+
20+
## Steps
21+
22+
### 1. Append `?debug_mode=true` to Your Local URL
23+
24+
Go to the page you want to testing the tracking event, and add parameter `?debug_mode=true`
25+
Example:
26+
27+
```
28+
http://localhost:3000/queue_time_analysis?debug_mode=true
29+
```
30+
31+
you should see the QA debugging info in console:
32+
33+
### View debug view in Google Analytics
34+
35+
[Analytics DebugView](https://analytics.google.com/analytics/web/#/a44373548p420079840/admin/debugview/overview)
36+
37+
When click a tracking button or event, you should be able to see it logged in the debugview (it may have 2-15 secs delayed).
38+
39+
### Adding event to track
40+
41+
two options to add event:
42+
43+
#### data attribute
44+
45+
Provided customized listener to catch tracking event using data-attributes
46+
47+
This is used to track simple user behaviours.
48+
49+
```tsx
50+
Example usage:
51+
<button
52+
data-ga-action="signup_click"
53+
data-ga-label="nav_button"
54+
data-ga-category="cta"
55+
data-ga-event-types="click"
56+
>
57+
Sign Up
58+
</button>
59+
```
60+
61+
Supported data attributes:
62+
63+
- `data-ga-action` (required): GA action name
64+
- `data-ga-category` (optional): GA category (defaults to event type)
65+
- `data-ga-label` (optional): GA label
66+
- `data-ga-event-types` (optional): comma-separated list of allowed event types for this element (e.g. "click,submit")
67+
68+
#### using trackEventWithContext
69+
70+
using trackEventWithContext to provide extra content.
71+
72+
```tsx
73+
trackEventWithContext(
74+
action: string,
75+
category?: string,
76+
label?: string,
77+
extra?: Record<string, any>
78+
)
79+
```

torchci/lib/track.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { trackEventWithContext } from "./track";
2+
3+
/**
4+
* Sets up global GA event tracking for DOM elements using `data-ga-*` attributes.
5+
*
6+
* 🔍 This enables declarative analytics tracking by simply adding attributes to HTML elements.
7+
* You can limit tracking to specific DOM event types (e.g., "click") both globally and per-element.
8+
*
9+
* Example usage (in _app.tsx or layout):
10+
* useEffect(() => {
11+
* const teardown = setupGAAttributeEventTracking(["click", "submit"]);
12+
* return teardown; // cleanup on unmount
13+
* }, []);
14+
*
15+
* Example usage:
16+
* <button
17+
* data-ga-action="signup_click"
18+
* data-ga-label="nav_button"
19+
* data-ga-category="cta"
20+
* data-ga-event-types="click"
21+
* >
22+
* Sign Up
23+
* </button>
24+
*
25+
* Supported data attributes:
26+
* - `data-ga-action` (required): GA action name
27+
* - `data-ga-label` (optional): GA label
28+
* - `data-ga-category` (optional): GA category (defaults to event type)
29+
* - `data-ga-event-types` (optional): comma-separated list of allowed event types for this element (e.g. "click,submit")
30+
*
31+
* @param globalEventTypes - Array of DOM event types to listen for globally (default: ["click", "change", "submit", "mouseenter"])
32+
* @returns Cleanup function to remove all added event listeners
33+
*/
34+
export function setupGAAttributeEventTracking(
35+
globalEventTypes: string[] = ["click", "change", "submit", "mouseenter"]
36+
): () => void {
37+
const handler = (e: Event) => {
38+
const target = e.target as HTMLElement | null;
39+
if (!target) return;
40+
41+
const el = target.closest("[data-ga-action]") as HTMLElement | null;
42+
if (!el) return;
43+
44+
const action = el.dataset.gaAction;
45+
if (!action) return;
46+
47+
// Check if this element has a restricted set of allowed event types
48+
const allowedTypes = el.dataset.gaEventTypes
49+
?.split(",")
50+
.map((t) => t.trim());
51+
if (allowedTypes && !allowedTypes.includes(e.type)) {
52+
return; // This event type is not allowed for this element
53+
}
54+
55+
const label = el.dataset.gaLabel;
56+
const category = el.dataset.gaCategory || e.type; // Default category to event type if not provided
57+
58+
// Construct event parameters for GA4
59+
const eventParams = {
60+
category,
61+
label,
62+
url: window.location.href,
63+
windowPathname: window.location.pathname,
64+
};
65+
66+
trackEventWithContext(action, category, label);
67+
};
68+
69+
// Add event listeners
70+
globalEventTypes.forEach((eventType) => {
71+
document.addEventListener(eventType, handler, true); // Use `true` for capture phase to catch events early
72+
});
73+
74+
// Return cleanup function
75+
return () => {
76+
globalEventTypes.forEach((eventType) => {
77+
document.removeEventListener(eventType, handler, true);
78+
});
79+
};
80+
}

torchci/lib/tracking/track.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { NextRouter } from "next/router";
2+
import ReactGA from "react-ga4";
3+
4+
const GA_SESSION_ID = "ga_session_id";
5+
const GA_MEASUREMENT_ID = "G-HZEXJ323ZF";
6+
7+
// Add a global flag to window object
8+
declare global {
9+
interface Window {
10+
__GA_INITIALIZED__?: boolean;
11+
gtag?: (...args: any[]) => void; // Declare gtag for direct access check
12+
}
13+
}
14+
15+
export const isGaInitialized = (): boolean => {
16+
return typeof window !== "undefined" && !!window.__GA_INITIALIZED__;
17+
};
18+
19+
function isDebugMode() {
20+
return (
21+
typeof window !== "undefined" &&
22+
window.location.search.includes("debug_mode=true")
23+
);
24+
}
25+
26+
function isProdEnv() {
27+
return (
28+
typeof window !== "undefined" &&
29+
window.location.href.startsWith("https://hud.pytorch.org")
30+
);
31+
}
32+
33+
function isGAEnabled(): boolean {
34+
if (typeof window === "undefined") return false;
35+
return isDebugMode() || isProdEnv();
36+
}
37+
38+
/**
39+
* initialize google analytics
40+
* if withUserId is set, we generate random sessionId to track action sequence for a single page flow.
41+
* Notice, we use session storage, if user create a new page tab due to navigation, it's considered new session
42+
* @param withUserId
43+
* @returns
44+
*/
45+
export const initGaAnalytics = (withSessionId = false) => {
46+
// Block in non-production deployments unless the debug_mode is set to true in url.
47+
if (!isGAEnabled()) {
48+
console.info("[GA] Skipping GA init");
49+
return;
50+
}
51+
52+
if (isGaInitialized()) {
53+
console.log("ReactGA already initialized.");
54+
return;
55+
}
56+
57+
ReactGA.initialize(GA_MEASUREMENT_ID, {
58+
// For enabling debug mode for GA4, the primary option is `debug: true`
59+
// passed directly to ReactGA.initialize.
60+
// The `gaOptions` and `gtagOptions` are for more advanced configurations
61+
// directly passed to the underlying GA/Gtag library.
62+
// @ts-ignore
63+
debug: isDebugMode(),
64+
gaOptions: {
65+
debug_mode: isDebugMode(),
66+
},
67+
gtagOptions: {
68+
debug_mode: isDebugMode(),
69+
cookie_domain: isDebugMode() ? "none" : "auto",
70+
},
71+
});
72+
73+
window.__GA_INITIALIZED__ = true; // Set a global flag
74+
75+
// generate random userId in session storage.
76+
if (withSessionId) {
77+
let id = sessionStorage.getItem(GA_SESSION_ID);
78+
if (!id) {
79+
id = crypto.randomUUID();
80+
sessionStorage.setItem(GA_SESSION_ID, id);
81+
}
82+
ReactGA.set({ user_id: id });
83+
}
84+
};
85+
86+
export function trackRouteEvent(
87+
router: NextRouter,
88+
eventName: string,
89+
info: Record<string, any> = {}
90+
) {
91+
if (!isGAEnabled()) {
92+
return;
93+
}
94+
95+
const payload = {
96+
...info,
97+
url: window.location.href,
98+
windowPathname: window.location.pathname,
99+
routerPathname: router.pathname,
100+
routerPath: router.asPath,
101+
...(isDebugMode() ? { debug_mode: true } : {}),
102+
};
103+
104+
ReactGA.event(eventName.toLowerCase(), payload);
105+
}
106+
107+
/**
108+
* track event with context using QA
109+
* @param action
110+
* @param category
111+
* @param label
112+
* @param extra
113+
* @returns
114+
*/
115+
export function trackEventWithContext(
116+
action: string,
117+
category?: string,
118+
label?: string,
119+
extra?: Record<string, any>
120+
) {
121+
if (!isGAEnabled()) {
122+
return;
123+
}
124+
const payload = {
125+
category,
126+
label,
127+
event_time: new Date().toISOString(),
128+
page_title: document.title,
129+
session_id: sessionStorage.getItem(GA_SESSION_ID) ?? undefined,
130+
131+
...(isDebugMode() ? { debug_mode: true } : {}),
132+
};
133+
ReactGA.event(action, payload);
134+
}

torchci/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"next": "14.2.30",
5353
"next-auth": "^4.24.5",
5454
"octokit": "^1.7.1",
55+
"pino-std-serializers": "^7.0.0",
5556
"probot": "^12.3.3",
5657
"react": "^18.3.1",
5758
"react-dom": "^18.3.1",

0 commit comments

Comments
 (0)