Skip to content

Add event tracking use GA #6940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions torchci/components/commit/WorkflowBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ function WorkflowJobSummary({
<JobButton
variant="outlined"
href={`/utilization/${m.workflow_id}/${m.job_id}/${m.run_attempt}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how to us the data-ga atrributes

data-ga-action="utilization_report_click"
data-ga-label="nav_button"
data-ga-category="user_interaction"
data-ga-event-types="click"
>
Utilization Report{" "}
</JobButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,6 @@ const getPercentileLineChart = (
const lines = [];
const date = params[0].axisValue;
lines.push(`<b>${date}</b>`);
console.log(lines);
for (const item of params) {
const idx = item.data;
const lineName = item.seriesName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { propsReducer } from "components/benchmark/llms/context/BenchmarkProps";
import { DateRangePicker } from "components/queueTimeAnalysis/components/pickers/DateRangePicker";
import { TimeGranuityPicker } from "components/queueTimeAnalysis/components/pickers/TimeGranuityPicker";
import dayjs from "dayjs";
import { trackEventWithContext } from "lib/tracking/track";
import { cloneDeep } from "lodash";
import { NextRouter } from "next/router";
import { ParsedUrlQuery } from "querystring";
Expand Down Expand Up @@ -232,6 +233,9 @@ export default function QueueTimeSearchBar({

const onSearch = () => {
const newprops = cloneDeep(props);
trackEventWithContext("qta_search", "user_interaction", "button_click", {
data: newprops.category,
});
updateSearch({ type: "UPDATE_FIELDS", payload: newprops });
};

Expand Down Expand Up @@ -330,7 +334,15 @@ export default function QueueTimeSearchBar({
</ScrollBar>
<SearchButton>
<Box sx={{ borderBottom: "1px solid #eee", padding: "0 0" }} />
<RainbowButton onClick={onSearch}>Search</RainbowButton>
<RainbowButton
data-ga-action="qta_search_click"
data-ga-label="search_button"
data-ga-category="cta"
data-ga-event-types="click"
onClick={onSearch}
>
Search
</RainbowButton>
<FormHelperText>
<span style={{ color: "red" }}>*</span> Click to apply filter
changes
Expand Down
79 changes: 79 additions & 0 deletions torchci/docs/event_tracking_guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Event Tracking guidance

# Overview

Guidance for event tracking in torchci.

## Google Analytics Development Guide (Local Development)

### Overview

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.

### Prerequisites

- TorchCI front-end development environment is set up and running locally
- Chrome browser installed
- Install chrome extension [Google Analytics Debugger](https://chrome.google.com/webstore/detail/jnkmfdileelhofjcijamephohjechhna)
- 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

## Steps

### 1. Append `?debug_mode=true` to Your Local URL

Go to the page you want to testing the tracking event, and add parameter `?debug_mode=true`
Example:

```
http://localhost:3000/queue_time_analysis?debug_mode=true
```

you should see the QA debugging info in console:

### View debug view in Google Analytics

[Analytics DebugView](https://analytics.google.com/analytics/web/#/a44373548p420079840/admin/debugview/overview)

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).

### Adding event to track

two options to add event:

#### data attribute

Provided customized listener to catch tracking event using data-attributes

This is used to track simple user behaviours.

```tsx
Example usage:
<button
data-ga-action="signup_click"
data-ga-label="nav_button"
data-ga-category="cta"
data-ga-event-types="click"
>
Sign Up
</button>
```

Supported data attributes:

- `data-ga-action` (required): GA action name
- `data-ga-category` (optional): GA category (defaults to event type)
- `data-ga-label` (optional): GA label
- `data-ga-event-types` (optional): comma-separated list of allowed event types for this element (e.g. "click,submit")

#### using trackEventWithContext

using trackEventWithContext to provide extra content.

```tsx
trackEventWithContext(
action: string,
category?: string,
label?: string,
extra?: Record<string, any>
)
```
15 changes: 0 additions & 15 deletions torchci/lib/track.ts

This file was deleted.

80 changes: 80 additions & 0 deletions torchci/lib/tracking/eventTrackingHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { trackEventWithContext } from "./track";

/**
* Sets up global GA event tracking for DOM elements using `data-ga-*` attributes.
*
* 🔍 This enables declarative analytics tracking by simply adding attributes to HTML elements.
* You can limit tracking to specific DOM event types (e.g., "click") both globally and per-element.
*
* Example usage (in _app.tsx or layout):
* useEffect(() => {
* const teardown = setupGAAttributeEventTracking(["click", "submit"]);
* return teardown; // cleanup on unmount
* }, []);
*
* Example usage:
* <button
* data-ga-action="signup_click"
* data-ga-label="nav_button"
* data-ga-category="cta"
* data-ga-event-types="click"
* >
* Sign Up
* </button>
*
* Supported data attributes:
* - `data-ga-action` (required): GA action name
* - `data-ga-label` (optional): GA label
* - `data-ga-category` (optional): GA category (defaults to event type)
* - `data-ga-event-types` (optional): comma-separated list of allowed event types for this element (e.g. "click,submit")
*
* @param globalEventTypes - Array of DOM event types to listen for globally (default: ["click", "change", "submit", "mouseenter"])
* @returns Cleanup function to remove all added event listeners
*/
export function setupGAAttributeEventTracking(
globalEventTypes: string[] = ["click", "change", "submit", "mouseenter"]
): () => void {
const handler = (e: Event) => {
const target = e.target as HTMLElement | null;
if (!target) return;

const el = target.closest("[data-ga-action]") as HTMLElement | null;
if (!el) return;

const action = el.dataset.gaAction;
if (!action) return;

// Check if this element has a restricted set of allowed event types
const allowedTypes = el.dataset.gaEventTypes
?.split(",")
.map((t) => t.trim());
if (allowedTypes && !allowedTypes.includes(e.type)) {
return; // This event type is not allowed for this element
}

const label = el.dataset.gaLabel;
const category = el.dataset.gaCategory || e.type; // Default category to event type if not provided

// Construct event parameters for GA4
const eventParams = {
category,
label,
url: window.location.href,
windowPathname: window.location.pathname,
};

trackEventWithContext(action, category, label);
};

// Add event listeners
globalEventTypes.forEach((eventType) => {
document.addEventListener(eventType, handler, true); // Use `true` for capture phase to catch events early
});

// Return cleanup function
return () => {
globalEventTypes.forEach((eventType) => {
document.removeEventListener(eventType, handler, true);
});
};
}
134 changes: 134 additions & 0 deletions torchci/lib/tracking/track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { NextRouter } from "next/router";
import ReactGA from "react-ga4";

const GA_SESSION_ID = "ga_session_id";
const GA_MEASUREMENT_ID = "G-HZEXJ323ZF";

// Add a global flag to window object
declare global {
interface Window {
__GA_INITIALIZED__?: boolean;
gtag?: (...args: any[]) => void; // Declare gtag for direct access check
}
}

export const isGaInitialized = (): boolean => {
return typeof window !== "undefined" && !!window.__GA_INITIALIZED__;
};

function isDebugMode() {
return (
typeof window !== "undefined" &&
window.location.search.includes("debug_mode=true")
);
}

function isProdEnv() {
return (
typeof window !== "undefined" &&
window.location.href.startsWith("https://hud.pytorch.org")
);
}

function isGAEnabled(): boolean {
if (typeof window === "undefined") return false;
return isDebugMode() || isProdEnv();
}

/**
* initialize google analytics
* if withUserId is set, we generate random sessionId to track action sequence for a single page flow.
* Notice, we use session storage, if user create a new page tab due to navigation, it's considered new session
* @param withUserId
* @returns
*/
export const initGaAnalytics = (withSessionId = false) => {
// Block in non-production deployments unless the debug_mode is set to true in url.
if (!isGAEnabled()) {
console.info("[GA] Skipping GA init");
return;
}

if (isGaInitialized()) {
console.log("ReactGA already initialized.");
return;
}

ReactGA.initialize(GA_MEASUREMENT_ID, {
// For enabling debug mode for GA4, the primary option is `debug: true`
// passed directly to ReactGA.initialize.
// The `gaOptions` and `gtagOptions` are for more advanced configurations
// directly passed to the underlying GA/Gtag library.
// @ts-ignore
debug: isDebugMode(),
gaOptions: {
debug_mode: isDebugMode(),
},
gtagOptions: {
debug_mode: isDebugMode(),
cookie_domain: isDebugMode() ? "none" : "auto",
},
});

window.__GA_INITIALIZED__ = true; // Set a global flag

// generate random userId in session storage.
if (withSessionId) {
let id = sessionStorage.getItem(GA_SESSION_ID);
if (!id) {
id = crypto.randomUUID();
sessionStorage.setItem(GA_SESSION_ID, id);
}
ReactGA.set({ user_id: id });
}
};

export function trackRouteEvent(
router: NextRouter,
eventName: string,
info: Record<string, any> = {}
) {
if (!isGAEnabled()) {
return;
}

const payload = {
...info,
url: window.location.href,
windowPathname: window.location.pathname,
routerPathname: router.pathname,
routerPath: router.asPath,
...(isDebugMode() ? { debug_mode: true } : {}),
};

ReactGA.event(eventName.toLowerCase(), payload);
}

/**
* track event with context using QA
* @param action
* @param category
* @param label
* @param extra
* @returns
*/
export function trackEventWithContext(
action: string,
category?: string,
label?: string,
extra?: Record<string, any>
) {
if (!isGAEnabled()) {
return;
}
const payload = {
category,
label,
event_time: new Date().toISOString(),
page_title: document.title,
session_id: sessionStorage.getItem(GA_SESSION_ID) ?? undefined,

...(isDebugMode() ? { debug_mode: true } : {}),
};
ReactGA.event(action, payload);
}
1 change: 1 addition & 0 deletions torchci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"next": "14.2.30",
"next-auth": "^4.24.5",
"octokit": "^1.7.1",
"pino-std-serializers": "^7.0.0",
"probot": "^12.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
Loading