Skip to main content

GA4 event parameter reference

This page holds the allowed values for our GA4 event parameters. The other analytics pages explain the theory. This one says what to send.

Until now these values lived only in a Google Sheet that nobody maintained. They are copied here so the docs are the source of truth.

Which spreadsheet sheet was correct

The workbook jodapp-google-events-map.xlsx has 9 sheets. Three of them look like they could be the spec. Only one is.

SheetVerdictWhy
recommended-eventsUse this oneMatches all 9 events in the code. Holds the January 2026 decisions. All four dated comment threads are here
event-param-mapStalePre-January 2026. Has no job_apply_start. Still requires user_persona on generate_lead, which was removed. 27 of 34 rows have no parameters
redoA proposal, not a recordAI-generated rewrite. Missing login and sign_up. Still uses the old partner value. Refers to a CSV outside the workbook
item-mapUse — companionThe item object spec
item-list-idsUse — companionThe item_list_id catalogue
content-typesUse — companionThe content_type enum
auto-collected-eventsIgnoreA copy of Google's own documentation
OverviewIgnoreA rough flow sketch. No parameters
custom-eventsIgnoreEmpty. Header row only

Three checks proved recommended-events is the one the code was written from:

  • It contains job_apply_start with the note "jan 2026 / will redirect to gig-partners.jodapp.com". It also moves add_to_cart to Future and redefines it as company credit top-up. That matches commit 44c99eac on 2026-01-15, which replaced add_to_cart with job_apply_start.
  • login is the only event in that sheet with no user_group parameter. In analytics-service.js, login is the only event that does not push user_group. The same single exception in both places.
  • A comment thread on that sheet dated 2026-01-15 decided that currency moves out of the event and into the item object. The shipped code does exactly that.

google-event-mapper.js:13 links one specific tab by gid, which confirms a single tab was treated as the contract.

The nine events the app sends

These are defined in jodapp-web/app/domains/analytics/analytics-service.js.

EventWhat it means
view_item_listA list of job cards scrolled into view
select_itemSomeone clicked a job card
view_itemA job detail page loaded
job_apply_startThe apply box opened. Custom event
generate_leadA careers application was accepted
searchA job search ran
shareA job was shared
loginA login succeeded
sign_upA signup succeeded

Allowed values

user_group

ValueMeaning
guestNot logged in
talentA worker
orgAn employer

The spreadsheet used the word partner for workers. The code renamed it to talent in commit 31382490 on 2026-01-22. Use talent.

The org value is almost never sent

use-auth.jsx:46 decides the value like this:

const userGroup = isLoggedIn() ? USER_GROUP.TALENT : USER_GROUP.GUEST

It never checks orgMembership, even though that value is available in the same file. So every logged-in employer is currently tagged talent on view_item, view_item_list, select_item, share and generate_lead. Only org-sign-up-page.jsx:42 ever sends org.

The intended rule, from the spreadsheet, is:

  • guest — not logged in
  • org — logged in and has an org user profile
  • talent — logged in and has a career profile, or is on gig-partners

Fix this before registering user_group as a custom dimension. Otherwise the dimension will be wrong from day one.

user_persona

ValueMeaning
guestNot logged in
org_profileEmployer profile
gig_profileGig worker profile
career_profileCareers worker profile

Do not send this yet. The spreadsheet says Rails has no gig_profile today, so it is held back until gig moves to jodapp-api. The code correctly does not send it.

share.method

Value
whatsapp
telegram
linkedin
facebook
twitter
email
copy_link

content_type

ValueRails model
gig_jobGig::TempJob
careers_jobCareers::Job
mkt_campaignMarketing::Campaign — future

job_pay_type

Value
hourly_rate
daily_rate
monthly_rate
annual_salary
annual_plus_commission
The spreadsheet has this one wrong

The spreadsheet lists the short forms hourly, daily, monthly, annually, annually_plus_commission. Do not use those. A live payload captured in Tag Assistant on 2026-07-30 shows job_pay_type: "hourly_rate".

The schema confirms it. jodapp.dbml:360 and :664 both define rails_enum(:hourly_rate, :daily_rate, :monthly_rate, :annual_salary, :annual_plus_commission).

The short forms are a different columnsalary_expectations_pay_type on the talent profile (talent.dbml:40), which really is hourly, daily, monthly, annual. The two were mixed up.

One inconsistency in the schema itself: line 360 writes annual_plus_commissions with an s, line 664 writes annual_plus_commission without. Check a real row before you rely on either.

lead_source

ValueMeaning
job_applicationA worker applied to a job
org_signupAn employer signed up. Future

The value and currency parameters are only required when lead_source is org_signup. The code sends them always, with value set to 0. That is harmless.

item_list_id

ValuePageLive today?
search_results/jobsYes
home_careers_jobs/Yes
home_gig_jobs/Yes
bookmarked_jobs/talent/job-bookmarksYes
mcdonalds_search/jobs/mcdonaldsYes

The spreadsheet lists five more values marked "future".

item_category

Value
Careers::Job
Gig::Job
These break our own naming rule

The spreadsheet states a principle: hide Rails class names from reports. Turn Gig::Job into gig_job. The code follows this for content_type but not for item_category or job_category, which send the raw Rails strings.

We have not changed this. Changing it now would split every historical report at the cut-over date. Decide deliberately before touching it.

Value formats

ParameterFormatExample
item_idlisting_job_ then the idlisting_job_1234
item_id for campaignsmkt_campaign_ then the slugmkt_campaign_cny-2025-02
item_variant and job_shiftDDD HH:MM - HH:MMFri 08:00 - 17:00
Overnight shiftSame, with a day-plus markerMon 23:00 - 07:00 (+1)
geo_area_admin_level_0Country code, lower casesg
geo_area_admin_level_1Region, lower case with underscoreseast_region
geo_area_admin_level_2Area name, lower casepasir ris
currency and job_pay_currencyThree letters. We send lower casesgd

For a Careers::Job there is no shift, so item_variant carries the employment type instead. A live payload on 2026-07-30 showed job_shift: "part_time" for a careers job, which is the employment type as expected.

We send currency in lower case, and Google expects upper case

A live payload on 2026-07-30 showed job_pay_currency: "sgd". GA4 expects ISO 4217 in upper case, so SGD.

This does no harm today, because our Financial Purity rule keeps value at 0 on every event, so there is no revenue for GA4 to mis-handle. It would start to matter the moment we send a real amount, for example when company credit top-up lands. Fix it before then, not after.

The geo values are built by splitting geo_area_path on the dot character. The path looks like sg.west_region.jurong_east.jurong_gateway.

Rules that are easy to get wrong

Money must stay pure

The spreadsheet names this "Principle D: Financial Purity". GA4 revenue reports must only ever receive real money, which means company credit top-ups. A job application is not revenue.

So:

  • Never send price on a job item.
  • Always send value as 0 on view_item and generate_lead.
  • purchase and refund are reserved for company credit top-up only.
  • add_to_cart and begin_checkout are reserved for the credit top-up flow. This is why the apply-click event is the custom job_apply_start and not add_to_cart.

Two Google events we must never use

Do not useUse insteadReason
earn_virtual_currencyloyalty_earnTurns on GA4's game reporting templates, which pollute our B2B data
spend_virtual_currencycredit_spendSame reason

The index parameter

index is the position of a card in a list. With paging, the formula is:

index = position_on_page + (page_number * per_page)

Do not send index on view_item, because a detail page shows one job with no list position.

quantity

Send quantity only on add_to_cart and begin_checkout. Leave it off every browsing event.

This exists nowhere in the code. Shared-link traffic cannot be traced back to the job that was shared until it is built.

ParameterValue
utm_sourceThe share method the user picked, for example whatsapp
utm_mediumsocial_share
utm_campaignshare_job on jodapp-web, share_shift on gig-partners, or share_mkt_campaign
utm_contentThe same string as item_id

What is specified, built, and actually measured

Counts cover the whole life of the property, 9 December 2025 to 29 July 2026.

EventIn the spec?In the code?Arrives in GA4?Events
view_item_listYesYesYes119,143
view_itemYesYesYes68,929
select_itemYesYesYes49,994
generate_leadYesYesYes1,688
searchYesYesNo0
shareYesYesNo0
job_apply_startYesYesNo0
loginYesYesNo0
sign_upYesYesNo0
add_to_wishlistYes, priority 2Deleted 2026-01-26No0
ad_impressionMarked "future"NoYes159,808
ad_clickNot listedNoYes1,361
scroll_depthNot listedNoYes313,198
view_item_jodappNot listedNoYes64,807
page_view_jodapp_job_detailNot listedNoYes96

Four things this table shows.

1. Five of the nine built events never reach GA4. All five have real callers in the app, and the dataLayer.push runs. The cause is in GTM, not in the code.

Container GTM-NKWB9VWN was read through the Tag Manager API on 2026-07-30. It holds 6 tags and 3 triggers. One generic tag, GA4 Event - Ecommerce, forwards our app events using eventName: {{Event}}. Its trigger only matches these names:

^(view_item|view_item_list|select_item|add_to_cart|begin_checkout|purchase|generate_lead)$

None of the five silent events are in that pattern. Note that add_to_cart is still allowed, although the app stopped sending it on 2026-01-15 when commit 44c99eac replaced it with job_apply_start. The app changed and the trigger did not.

job_apply_start is the costly one, because it is the missing middle of the apply funnel. See the funnel report and jodapp-web issue #1349.

Fix built on 2026-07-30 and verified in Tag Assistant. The container now has:

  • job_apply_start added to the ecommerce trigger pattern.
  • A new trigger GA4 - Custom App Events matching ^(login|sign_up|search|share)$.
  • A new tag GA4 Event - Custom, with ecommerce sending off, for those four.

All four were confirmed firing with "Succeeded" in Tag Assistant.

2. Parameters must pass two separate gates. This is the part that surprises people, so state it plainly.

GateWho controls itWhat happens if it fails
Collection — does the value arrive?GTMThe value is dropped in the browser. GA4 never sees it
Reporting — can you query it?GA4GA4 stores the value on the event, but it appears in no report and no exploration

Passing the first gate does nothing on its own.

Until 2026-07-30 we failed both. GTM had no Data Layer Variables at all, so no flat parameter was ever collected — not even for the four events that worked. Only the ecommerce object got through, because sendEcommerceData: true bulk-copies that one sub-object without needing a variable per field. That is why job type and list name could be analysed while user_group could not.

Collection is now fixed. Ten Data Layer Variables were added, bundled into two Event Settings Variables and attached to the two event tags:

  • GA4 - Job Event Parametersuser_group, job_id, job_category, job_company, job_pay_amount, job_pay_type, lead_source
  • GA4 - Auth & Share Parametersuser_group, search_term, method, content_type

They are split on purpose. The data layer is a cumulative merged model: a value stays in it until something overwrites it. A single shared list would let login inherit a job_id from an earlier view_item, which looks like real data and is not.

Reporting is still open. The property has zero registered custom dimensions, so none of these parameters can be used in a report yet. Register them in GA4 Admin under Custom definitions. Two rules:

  • A custom dimension never backfills. It collects only from the day you create it, so register promptly.
  • Fix the user_group bug first. use-auth.jsx:46 tags every logged-in employer as talent. Register the dimension while that is live and the wrong values become permanent history.

3. ad_impression fires 159,808 times but the spec marks it "future". An ad integration went live without the spec being updated. AdSense publisher ca-pub-8191136256481391 is injected in production.

4. view_item_jodapp is not a duplicate tag. It looks like one, sitting at 64,807 against view_item at 68,929. It is not a duplicate, and it is not built in GTM. The container holds no tag with that name.

It is a GA4-side rule instead, created under GA4 Admin then Events then Create event. The rule copies view_item when page_location starts with https://jodapp.com/, for use as a Google Ads conversion. The 4,122 difference is the view_item events from gig-partners, which the rule correctly excludes.

The same is true of page_view_jodapp_job_detail. Leave both alone. If you are looking for where they are defined, look in GA4, not in GTM.

Cells in the spreadsheet that are behind the code

Do not copy these forward.

WhereWhat it saysWhat is true
recommended-events row 101add_to_wishlist is applicable, priority 2Deleted from the code on 2026-01-26, commit af84f044
recommended-events J23, J41ecommerce.currency is required on the eventMoved inside the item object. Decided in that sheet's own comment thread on 2026-01-15
item-map rows 20–21job_pay_amount and job_pay_type sit on the itemRemoved from toItem() in commit 9342b7c7 on 2026-01-22. They are bridge parameters now
item-mapThe item has no currencyThe item does carry currency. See google-event-mapper.js:103
event-param-map D25generate_lead requires user_personaRemoved on 2026-01-22

Known code gaps

GapWhereEffect
Employers tagged as talentuse-auth.jsx:46user_group is wrong for every logged-in employer
job_company is sent but is not in the specgoogle-event-mapper.js bridge paramsHarmless. Add it to the spec
select_item on bookmarks omits user_groupcareers-job-bookmarks-list-page.jsx:139That one surface has no user group
ecommerce is never reset with ecommerce: nullAll pushesNo leak found today. Re-check if we ever send multi-item events
No tracking on /companies/:slug and /collections/:slugThose routesBoth show job lists and are invisible in GA4