Jodapp.com Analytics architecture
Generated by Gemini 3.0 Pro Reviewed by ali
1. Executive Summary & Mental Models
Standard analytics setups fail for Jodapp because we are both a Retail Store (for Workers) and a B2B SaaS (for Companies). To solve this, we operate on four core Mental Models.
- Principle A: Two Sided Economy
- Principle B: Jobs are Products (Discovery Model)
- Principle C: Financial Purity (Real vs Virtual)
- Principle D: Fluid Identity (Active Persona)
Principle A: The "Two-Sided" Economy
We run two distinct funnels in one property.
- Supply Side (Workers): High-volume, "Shopping" behavior. Workers browse and "buy" shifts with their time.
- Demand Side (Companies): Low-volume, B2B behavior. Companies buy credits (Revenue) and manage inventory.
Principle B: Jobs are Products (The Discovery Model)
Since workers behave like shoppers, we map Listing::Job directly to the GA4 Item (Product) Schema.
- Why? This unlocks GA4's E-commerce Shopping Funnel (
view_itemadd_to_cartcheckout), allowing us to visualize exactly where workers drop off.
Principle C: Financial Purity (Real vs. Virtual)
We strictly separate Real Money from Platform Activity.
purchaseEvent: Reserved strictly for Real Money transactions (Company Credit Top-ups).generate_leadEvent: Used for Worker Applications ($0 value) and Company Sign-ups to prevent polluting ROAS (Return on Ad Spend) reports.
Principle D: The "Fluid Identity" (Active Persona)
A single human (Identities::User) can be a Gig Worker today and an Employer tomorrow.
- Implication: We do not track users with `id. We track the Active Persona of the session.
- Implementation: Every single event is sliced by
user_group(Worker vs. Employer) anduser_persona(Gig vs. Career).
We must treat the user_persona not as a permanent label of the human, but as the Active Context of their current session.
- When I log in to my Gig Dashboard, my persona is
gig_profile. - If I switch to my Career Dashboard, my persona changes to
career_profile
To slice data ("Org vs User" AND "Gig vs Career"), we should split this into two distinct Custom Dimensions.
user_groupuser_persona
Dimension A: user_group (The High-Level Slice)
- Purpose: Separates the Supply Side (Workers) from the Demand Side (Employers).
- Values:
partner|org - Implementation:
Org::UserProfile:orgGig::UserProfile:partnerCareers::UserProfile:partner
Dimension B: user_persona (The Granular Slice)
- Purpose: Separates the context of the worker.
- Values:
gig_profile|careers_profile|org_profile - Implementation: Maps 1:1 to backend profile.
2. High-Level Architecture (The Big Picture)
We track user flows across two domains: jodapp.com (React/Rails - The Showroom) and gig-partners.jodapp.com (Laravel - The Checkout).
Funnel A: Partner (Supply Side)
Goal: Volume of Shifts Filled.
| User Action | Event Name | Context | Logic |
|---|---|---|---|
| Sign Up | sign_up | jodapp.com | User creates account. |
| Login | login | jodapp.com | Not implemented in gig-partners due to different user_ids. User logs into jodapp account. |
| Search | search | jodapp.com | Submits a search term |
| Verify ID | partner_verified | - | Future: The Gatekeeper. User uploads docs & Admin approves. Happens BEFORE applying. |
| View Job List | view_item_list | jodapp.com | Browsing jobs within lists defined in items-list-id.gsheets (jodapp.com). |
| Select Job form List | select_item | jodapp.com | Selecting a job from a list (jodapp.com). |
| View Job Detail | view_item | jodapp.com | Viewing job details (jodapp.com). |
| Job Apply Start | job_apply_start | jodapp.com | Clicking apply button in jodapp.com, getting redirected to gig-partners |
| Submit App | generate_lead | gig-partners | The Conversion. On success of API to apply after user clicks apply in the pop-up apply T&C modal. |
| Get Hired | partner_selected | jodapp.com/employers | Future: Marketplace Match. Employer selects the worker. Custom Event. |
Funnel B: The Company (Demand Side)
Goal: Revenue & Inventory.
| User Action | Event Name | Logic & Context |
|---|---|---|
| Sign Up | generate_lead | The Lead. Company creates an account. |
| Verify Biz | qualify_lead | Quality Check. Admin verifies business. High-Value B2B Signal. |
| Buy Credits | purchase | The Revenue. Real money transfer. |
| Spend Credits | credit_spend | Inventory Usage. Credits deducted to hire worker. Custom Event. |
System Flow Diagram
3. Reporting Strategy & Data Slicing
Standard reports aggregate everyone together. To make data useful, we must "slice" it using Global Custom Dimensions.
3.1 Global Custom Dimensions
These must be registered in GA4 Admin and sent with every event.
| Dimension Key | Scope | Values | Purpose |
|---|---|---|---|
user_group | Event | guest, partner, org | Separates the Supply Side (Workers) from the Demand Side (Employers). |
user_persona | Event | guest, gig_profile, careers_profile, org_profile | Separates the context of the worker. |
job_type | Event | gig, careers | Separates the job type |
| Handling Unauthenticated Users (The "Guest" Rule) |
- Problem: If we send
nullfor users who aren't logged in, GA4 reports show(not set), which looks like a bug. - Solution: We explicitly assign the string
"guest"touser_groupanduser_personafor unauthenticated visitors. This allows us to track "Guest to Registered" conversion rates.
3.2 The "Shared Bucket" Strategy
We use generate_lead for BOTH Worker Applications and Company Sign-ups.
- How to Report: In GA4, go to the "Leads" report and apply a Filter:
user_groupexactly matchespartner. Now you see only Worker Applications.
4. Implementation Guide: The Partner Funnel
Step 1: Discovery (view_item_list)
- Context: Home Page, Search Page, Company Page shows lists of
Listings::Job - Rule: Send all the the
Listings::Jobin the list insideecommerce.items
Step 2: Low-level Intent (select_item)
- Context: User is interested in a single
Listings::Job - Rule: Send the
item_list_idanditem_list_namewith the selectedListings::Jobinecommerce.items
Step 3: Detailed Discovery (view_item)
- Context:
jodapp.comJob Detail Page. - Rule: Send ONE item object representing the Job Listing.
- Gotcha: Do NOT populate
item_variantwith a list of shifts here. This inflates view counts.
Step 4: High-Value Intent (job_apply_start)
- Context: User clicks "Apply" on
jodapp.com, and then is directed togig-partners.jodapp.com - Strategy: This captures the Specific Shift the user wants, and their high-level of intent to apply for the job.
- Purpose: Debugging. If
job_apply_start>generate_lead, then either redirect is broken or the funnel in `gig-partners.jodapp.com need to be reviewed - Rule: Include
item_varianthere because - Payload:
window.dataLayer.push({
event: "job_apply_start", // Custom Event
user_group: "partner",
// Custom Dimensions (Bridge Params)
job_id: `...`
job_title: `...`
job_category: `...`
job_category_2: `...`
job_variant: `...`
job_pay_amount: `...`
job_pay_type: `...`
job_pay_currency: `...`
// No ecommerce.items since default ecommerce reports cannot read custom events
});
Step 5: Conversion (generate_lead)
-
Context:
- User lands on
gig-partners.jodapp.com(Shift Detail Page). - User clicks Apply on the page.
- Modal opens for user accepts T&C.
- User clicks Apply (in the Modal) and we receive a 200 response, we trigger
generate_lead
- User lands on
-
Critical: Fire the
generate_leadevent AFTER the successful response from the job application. -
Payload:
// Fire on Page Load (Gig-Partners)
window.dataLayer.push({
event: "generate_lead",
user_group: "partner",
lead_source: "job_application", // Internal Context
// Custom Dimensions (Bridge Params)
job_id: `...`
job_title: `...`
job_category: `...`
job_category_2: `...`
job_variant: `...`
job_pay_amount: `...`
job_pay_type: `...`
job_pay_currency: `...`
ecommerce: {
items: [{
item_id: "JOB_123", // Read from URL Param
item_variant: "Fri 6pm-10pm" // Read from URL Param
}]
}
});
Step 4: Conversion (generate_lead)
- Context: User submits application on
gig-partners. - Payload:
window.dataLayer.push({
event: "generate_lead",
user_group: "partner",
lead_source: "job_application", // Internal Context
ecommerce: {
currency: "SGD",
value: 20.00,
items: [{
item_id: "listing_job_123",
item_name: "Waiter",
item_category: "Gig::Job",
item_variant: "Fri 6:00pm-10:00pm"
}]
}
});
Step 5: Verification & Matching (Custom Events)
-
worker_verified: Backend event when Admin approves docs. -
Why Custom? To distinguish from B2B
qualify_lead. -
hire_worker: Backend event when Employer selects worker. -
Why Custom? This is a "Marketplace Match," distinct from a B2B Lead Qualification.
5. Implementation Guide: The Company Funnel
The B2B Quality Signal (qualify_lead)
- Context: Admin verifies a Company is legitimate.
- Strict Rule: This event is ONLY for Company Verification.
- Payload:
window.dataLayer.push({
event: "qualify_lead",
user_group: "org",
lead_source: "org_signup",
value: 500.00, // High Value LTV Signal
currency: "SGD"
});
6. Google Ads Integration Strategy
We optimize for different outcomes based on the funnel.
| Campaign Goal | Event to Bid On (Key Event) | Reasoning |
|---|---|---|
| Worker Acquisition | worker_verified (Primary) | The Gatekeeper. Optimizes for users who pass ID checks. Faster feedback loop than waiting for a job application. |
| Worker Volume | generate_lead (Secondary) | Use if you need raw application volume regardless of quality. |
| Worker Success | hire_worker (Observation) | Great for audience building ("High Quality Workers"), but usually too low volume for Smart Bidding. |
| Company Acquisition | qualify_lead (Primary) | Optimizes for Verified Businesses (High Value). |
7. Implementation Checklist
- Backend Namespaces:
- Worker ID Verification
worker_verified - Company Business Verification
qualify_lead - Worker Hired
hire_worker
- Cross-Domain Handoff:
- Ensure
jodapp.compassesjob_idandvariantin the URL togig-partners. - Ensure
gig-partnersreads these params to populatebegin_checkoutandgenerate_lead.
- No Browsing on Legacy:
- Remove "Job Browsing" features from
gig-partnersto enforce the Showroom (React) vs. Checkout (Laravel) separation.
- GTM Configuration:
- Enable "Send Ecommerce Data > Data Layer" for the
generate_leadtag to support the item array automatically.