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.
| Sheet | Verdict | Why |
|---|---|---|
recommended-events | Use this one | Matches all 9 events in the code. Holds the January 2026 decisions. All four dated comment threads are here |
event-param-map | Stale | Pre-January 2026. Has no job_apply_start. Still requires user_persona on generate_lead, which was removed. 27 of 34 rows have no parameters |
redo | A proposal, not a record | AI-generated rewrite. Missing login and sign_up. Still uses the old partner value. Refers to a CSV outside the workbook |
item-map | Use — companion | The item object spec |
item-list-ids | Use — companion | The item_list_id catalogue |
content-types | Use — companion | The content_type enum |
auto-collected-events | Ignore | A copy of Google's own documentation |
Overview | Ignore | A rough flow sketch. No parameters |
custom-events | Ignore | Empty. Header row only |
Three checks proved recommended-events is the one the code was written from:
- It contains
job_apply_startwith the note "jan 2026 / will redirect to gig-partners.jodapp.com". It also movesadd_to_carttoFutureand redefines it as company credit top-up. That matches commit44c99eacon 2026-01-15, which replacedadd_to_cartwithjob_apply_start. loginis the only event in that sheet with nouser_groupparameter. Inanalytics-service.js,loginis the only event that does not pushuser_group. The same single exception in both places.- A comment thread on that sheet dated 2026-01-15 decided that
currencymoves 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.
| Event | What it means |
|---|---|
view_item_list | A list of job cards scrolled into view |
select_item | Someone clicked a job card |
view_item | A job detail page loaded |
job_apply_start | The apply box opened. Custom event |
generate_lead | A careers application was accepted |
search | A job search ran |
share | A job was shared |
login | A login succeeded |
sign_up | A signup succeeded |
Allowed values
user_group
| Value | Meaning |
|---|---|
guest | Not logged in |
talent | A worker |
org | An employer |
The spreadsheet used the word partner for workers. The code renamed it to
talent in commit 31382490 on 2026-01-22. Use talent.
org value is almost never sentuse-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 inorg— logged in and has an org user profiletalent— 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
| Value | Meaning |
|---|---|
guest | Not logged in |
org_profile | Employer profile |
gig_profile | Gig worker profile |
career_profile | Careers 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
| Value | Rails model |
|---|---|
gig_job | Gig::TempJob |
careers_job | Careers::Job |
mkt_campaign | Marketing::Campaign — future |
job_pay_type
| Value |
|---|
hourly_rate |
daily_rate |
monthly_rate |
annual_salary |
annual_plus_commission |
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 column — salary_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
| Value | Meaning |
|---|---|
job_application | A worker applied to a job |
org_signup | An 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
| Value | Page | Live today? |
|---|---|---|
search_results | /jobs | Yes |
home_careers_jobs | / | Yes |
home_gig_jobs | / | Yes |
bookmarked_jobs | /talent/job-bookmarks | Yes |
mcdonalds_search | /jobs/mcdonalds | Yes |
The spreadsheet lists five more values marked "future".
item_category
| Value |
|---|
Careers::Job |
Gig::Job |
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
| Parameter | Format | Example |
|---|---|---|
item_id | listing_job_ then the id | listing_job_1234 |
item_id for campaigns | mkt_campaign_ then the slug | mkt_campaign_cny-2025-02 |
item_variant and job_shift | DDD HH:MM - HH:MM | Fri 08:00 - 17:00 |
| Overnight shift | Same, with a day-plus marker | Mon 23:00 - 07:00 (+1) |
geo_area_admin_level_0 | Country code, lower case | sg |
geo_area_admin_level_1 | Region, lower case with underscores | east_region |
geo_area_admin_level_2 | Area name, lower case | pasir ris |
currency and job_pay_currency | Three letters. We send lower case | sgd |
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.
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
priceon a job item. - Always send
valueas0onview_itemandgenerate_lead. purchaseandrefundare reserved for company credit top-up only.add_to_cartandbegin_checkoutare reserved for the credit top-up flow. This is why the apply-click event is the customjob_apply_startand notadd_to_cart.
Two Google events we must never use
| Do not use | Use instead | Reason |
|---|---|---|
earn_virtual_currency | loyalty_earn | Turns on GA4's game reporting templates, which pollute our B2B data |
spend_virtual_currency | credit_spend | Same 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.
The UTM contract for shared links
This exists nowhere in the code. Shared-link traffic cannot be traced back to the job that was shared until it is built.
| Parameter | Value |
|---|---|
utm_source | The share method the user picked, for example whatsapp |
utm_medium | social_share |
utm_campaign | share_job on jodapp-web, share_shift on gig-partners, or share_mkt_campaign |
utm_content | The 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.
| Event | In the spec? | In the code? | Arrives in GA4? | Events |
|---|---|---|---|---|
view_item_list | Yes | Yes | Yes | 119,143 |
view_item | Yes | Yes | Yes | 68,929 |
select_item | Yes | Yes | Yes | 49,994 |
generate_lead | Yes | Yes | Yes | 1,688 |
search | Yes | Yes | No | 0 |
share | Yes | Yes | No | 0 |
job_apply_start | Yes | Yes | No | 0 |
login | Yes | Yes | No | 0 |
sign_up | Yes | Yes | No | 0 |
add_to_wishlist | Yes, priority 2 | Deleted 2026-01-26 | No | 0 |
ad_impression | Marked "future" | No | Yes | 159,808 |
ad_click | Not listed | No | Yes | 1,361 |
scroll_depth | Not listed | No | Yes | 313,198 |
view_item_jodapp | Not listed | No | Yes | 64,807 |
page_view_jodapp_job_detail | Not listed | No | Yes | 96 |
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_startadded to the ecommerce trigger pattern.- A new trigger
GA4 - Custom App Eventsmatching^(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.
| Gate | Who controls it | What happens if it fails |
|---|---|---|
| Collection — does the value arrive? | GTM | The value is dropped in the browser. GA4 never sees it |
| Reporting — can you query it? | GA4 | GA4 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 Parameters—user_group,job_id,job_category,job_company,job_pay_amount,job_pay_type,lead_sourceGA4 - Auth & Share Parameters—user_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_groupbug first.use-auth.jsx:46tags every logged-in employer astalent. 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.
| Where | What it says | What is true |
|---|---|---|
recommended-events row 101 | add_to_wishlist is applicable, priority 2 | Deleted from the code on 2026-01-26, commit af84f044 |
recommended-events J23, J41 | ecommerce.currency is required on the event | Moved inside the item object. Decided in that sheet's own comment thread on 2026-01-15 |
item-map rows 20–21 | job_pay_amount and job_pay_type sit on the item | Removed from toItem() in commit 9342b7c7 on 2026-01-22. They are bridge parameters now |
item-map | The item has no currency | The item does carry currency. See google-event-mapper.js:103 |
event-param-map D25 | generate_lead requires user_persona | Removed on 2026-01-22 |
Known code gaps
| Gap | Where | Effect |
|---|---|---|
Employers tagged as talent | use-auth.jsx:46 | user_group is wrong for every logged-in employer |
job_company is sent but is not in the spec | google-event-mapper.js bridge params | Harmless. Add it to the spec |
select_item on bookmarks omits user_group | careers-job-bookmarks-list-page.jsx:139 | That one surface has no user group |
ecommerce is never reset with ecommerce: null | All pushes | No leak found today. Re-check if we ever send multi-item events |
No tracking on /companies/:slug and /collections/:slug | Those routes | Both show job lists and are invisible in GA4 |