Skip to main content

Jodapp.com Analytics architecture

note

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.

  1. Supply Side (Workers): High-volume, "Shopping" behavior. Workers browse and "buy" shifts with their time.
  2. 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_item add_to_cart checkout), allowing us to visualize exactly where workers drop off.

Principle C: Financial Purity (Real vs. Virtual)

We strictly separate Real Money from Platform Activity.

  • purchase Event: Reserved strictly for Real Money transactions (Company Credit Top-ups).
  • generate_lead Event: 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) and user_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_group
  • user_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: org
    • Gig::UserProfile: partner
    • Careers::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 ActionEvent NameContextLogic
Sign Upsign_upjodapp.comUser creates account.
Loginloginjodapp.comNot implemented in gig-partners due to different user_ids. User logs into jodapp account.
Searchsearchjodapp.comSubmits a search term
Verify IDpartner_verified-Future: The Gatekeeper. User uploads docs & Admin approves. Happens BEFORE applying.
View Job Listview_item_listjodapp.comBrowsing jobs within lists defined in items-list-id.gsheets (jodapp.com).
Select Job form Listselect_itemjodapp.comSelecting a job from a list (jodapp.com).
View Job Detailview_itemjodapp.comViewing job details (jodapp.com).
Job Apply Startjob_apply_startjodapp.comClicking apply button in jodapp.com, getting redirected to gig-partners
Submit Appgenerate_leadgig-partnersThe Conversion. On success of API to apply after user clicks apply in the pop-up apply T&C modal.
Get Hiredpartner_selectedjodapp.com/employersFuture: Marketplace Match. Employer selects the worker. Custom Event.

Funnel B: The Company (Demand Side)

Goal: Revenue & Inventory.

User ActionEvent NameLogic & Context
Sign Upgenerate_leadThe Lead. Company creates an account.
Verify Bizqualify_leadQuality Check. Admin verifies business. High-Value B2B Signal.
Buy CreditspurchaseThe Revenue. Real money transfer.
Spend Creditscredit_spendInventory 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 KeyScopeValuesPurpose
user_groupEventguest, partner, orgSeparates the Supply Side (Workers) from the Demand Side (Employers).
user_personaEventguest, gig_profile, careers_profile, org_profileSeparates the context of the worker.
job_typeEventgig, careersSeparates the job type
Handling Unauthenticated Users (The "Guest" Rule)
  • Problem: If we send null for users who aren't logged in, GA4 reports show (not set), which looks like a bug.
  • Solution: We explicitly assign the string "guest" to user_group and user_persona for 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_group exactly matches partner. 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::Job in the list inside ecommerce.items

Step 2: Low-level Intent (select_item)

  • Context: User is interested in a single Listings::Job
  • Rule: Send the item_list_id and item_list_name with the selected Listings::Job in ecommerce.items

Step 3: Detailed Discovery (view_item)

  • Context: jodapp.com Job Detail Page.
  • Rule: Send ONE item object representing the Job Listing.
  • Gotcha: Do NOT populate item_variant with 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 to gig-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_variant here 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
  • Critical: Fire the generate_lead event 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 GoalEvent to Bid On (Key Event)Reasoning
Worker Acquisitionworker_verified (Primary)The Gatekeeper. Optimizes for users who pass ID checks. Faster feedback loop than waiting for a job application.
Worker Volumegenerate_lead (Secondary)Use if you need raw application volume regardless of quality.
Worker Successhire_worker (Observation)Great for audience building ("High Quality Workers"), but usually too low volume for Smart Bidding.
Company Acquisitionqualify_lead (Primary)Optimizes for Verified Businesses (High Value).

7. Implementation Checklist

  1. Backend Namespaces:
  • Worker ID Verification worker_verified
  • Company Business Verification qualify_lead
  • Worker Hired hire_worker
  1. Cross-Domain Handoff:
  • Ensure jodapp.com passes job_id and variant in the URL to gig-partners.
  • Ensure gig-partners reads these params to populate begin_checkout and generate_lead.
  1. No Browsing on Legacy:
  • Remove "Job Browsing" features from gig-partners to enforce the Showroom (React) vs. Checkout (Laravel) separation.
  1. GTM Configuration:
  • Enable "Send Ecommerce Data > Data Layer" for the generate_lead tag to support the item array automatically.