Frontend UTM Tracking Guidelines
This guide explains how to track frontend interactions with UTM parameters in both Vue components and vanilla JavaScript.
When to use UTM tracking
- Track clicks on important UI actions (buttons, menu actions, external links).
- Track GraphQL operations where campaign attribution is needed.
- Track elapsed time between form load and submit when this context matters.
Shared types and endpoint contract
- Frontend campaign/type definitions are in
app/frontend/src/shared/types/utm.ts. - REST tracking client is in
app/frontend/src/shared/api/utmApi.ts. - GraphQL URL helper is in
app/frontend/src/shared/utils/utm.ts. - Backend endpoint is
GET /utm_trackingineasy_engines/easy_extensions/config/routes.rb. - Endpoint action responds with
204 No Contentineasy_engines/easy_extensions/app/controllers/analytics_controller.rb.
1) Click tracking with REST endpoint
Use utmApi.sendUTMTrackingData(...) for regular click/event tracking.
Vue example
import { utmApi } from "@/src/shared/api/utmApi";
import { openUrlInNewTab } from "@/src/utils/safeNavigation";
const openWebinar = async () => {
await utmApi.sendUTMTrackingData({
utmCampaign: "news",
utmContent: "news_onboarding",
utmTerm: "webinar",
});
openUrlInNewTab("https://example.com/webinar");
};
Vanilla JavaScript example (no imports)
Use this when your script is built separately and you only want to call the REST endpoint.
async function trackUTM({ utmCampaign, utmContent, utmTerm }) {
const params = new URLSearchParams({
utm_campaign: utmCampaign,
utm_content: utmContent,
utm_term: utmTerm,
format: "json",
});
await fetch(`/utm_tracking?${params.toString()}`, {
method: "GET",
credentials: "same-origin",
});
}
document.getElementById("my-button")?.addEventListener("click", () => {
trackUTM({
utmCampaign: "menu",
utmContent: "react_widget",
utmTerm: "open_details",
}).catch(() => {});
});
React one-liner (no imports)
onClick={() => fetch(`/utm_tracking?${new URLSearchParams({ utm_campaign: "menu", utm_content: "react_widget", utm_term: "open_details", format: "json" })}`, { method: "GET", credentials: "same-origin" }).catch(() => {})}
2) GraphQL request tracking
Use getGraphqlUriWithUtmParams(...) when a GraphQL request should include UTM parameters.
import { getGraphqlUriWithUtmParams } from "@/src/shared/utils/utm";
const uri = getGraphqlUriWithUtmParams(
{
utm_campaign: "spent_time",
utm_content: "log_time_form_v2",
},
"create"
);
// Apollo context:
// context: { uri }
3) Elapsed time tracking (utm_time)
Vue composable approach
Use useUTMTimeTracker(...) to measure elapsed time and attach it to UTM params.
import { useUTMTimeTracker } from "@/src/shared/composables/useUTMTimeTracker";
const { getUTMParams, restartTimer } = useUTMTimeTracker({
utm_campaign: "spent_time",
utm_content: "log_time_form_v2",
});
const submit = () => {
const params = getUTMParams("create");
// params contains utm_time and utm_term
};
restartTimer();
Form helper approach (works outside Vue)
Use useUTMTimeOnForm(...) to update form action URL on submit with utm_campaign, utm_content, utm_term, and utm_time.
import { useUTMTimeOnForm } from "@/src/shared/utils/utm";
useUTMTimeOnForm("my-form-id", {
utmCampaign: "menu",
utmContent: "my_form",
utmTerm: "submit",
});
Naming conventions
utm_campaign: feature/domain name (must matchUTMCampaignunion).utm_content: UI area or action group.utm_term: specific action, state, or entity identifier.utm_time: elapsed milliseconds (string), when applicable.
Prefer constants for repeated values (for example constants/utm.ts in feature modules).
Optional inventory descriptions
The static UTM inventory can show a human-readable description when the tracked flow is unambiguous. Put an utm-inventory-description: marker on the single line immediately before the UTM declaration or call:
// utm-inventory-description: Creates a product backlog item from the product backlog board action.
export const PBI_CREATE_UTM = {
utmCampaign: "easy_scrum_boards",
utmContent: "pbi_create",
utmTerm: "button_product_backlog_board",
};
Ruby #, JavaScript/TypeScript //, and ERB <%# ... %> comments are supported. The annotation is optional; omit it for generic helpers, dynamic declarations, or flows whose meaning is unclear. An absent or empty marker produces an empty description and does not stop generation.
Descriptions are scanner metadata, not runtime tracking parameters. The scanner is regex-based: the marker must be on the immediately preceding source line, and annotations on reusable spread bases are not inherited by concrete objects. Annotate the concrete UTM object or the concrete source occurrence instead. A concrete occurrence annotation overrides reusable declaration metadata; conflicting nonblank descriptions at the same highest priority produce a blank description and warning.
Real examples in this repository
Click tracking
- Vue:
app/frontend/src/modals/newsOnboarding/components/NewsOnboardingActions.vue - Vue:
app/frontend/src/easy_help_feedback/EasyHelpFeedback.vue - Vanilla JS:
app/frontend/src/easy_legacy_js/automations_webhook.ts
GraphQL URI tracking
app/frontend/src/log_time/api/timeEntryApi.tsapp/frontend/src/easy_automations/rules_execution/api/rulesExecutionApi.ts
Shared utilities
app/frontend/src/shared/api/utmApi.tsapp/frontend/src/shared/utils/utm.tsapp/frontend/src/shared/composables/useUTMTimeTracker.ts
Tests
app/frontend/src/tests/tests/shared/tests/utils/utm.spec.tsapp/frontend/src/tests/tests/easy_legacy_js/automations_webhook.spec.tsapp/frontend/src/tests/tests/modals/tests/news_onboarding/tests/newsOnboardingActions.spec.ts