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.