Skip to content

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_tracking in easy_engines/easy_extensions/config/routes.rb.
  • Endpoint action responds with 204 No Content in easy_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 match UTMCampaign union).
  • 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.ts
  • app/frontend/src/easy_automations/rules_execution/api/rulesExecutionApi.ts

Shared utilities

  • app/frontend/src/shared/api/utmApi.ts
  • app/frontend/src/shared/utils/utm.ts
  • app/frontend/src/shared/composables/useUTMTimeTracker.ts

Tests

  • app/frontend/src/tests/tests/shared/tests/utils/utm.spec.ts
  • app/frontend/src/tests/tests/easy_legacy_js/automations_webhook.spec.ts
  • app/frontend/src/tests/tests/modals/tests/news_onboarding/tests/newsOnboardingActions.spec.ts