Skip to content

Location Spoofing Race Condition Bug โ€‹

Date: January 5, 2026
Status: โœ… FIXED - January 7, 2026
Priority: P0 - Must fix before wave testing can begin


Resolution Summary โ€‹

Fixed by: Moving initLocationSpoof() from App.jsx useEffect to main.jsx (synchronous init before React renders)

Changes Made:

  1. src/main.jsx - Added synchronous call to initLocationSpoof() before React renders
  2. src/App.jsx - Removed duplicate useEffect that called initLocationSpoof()

Result: Location spoofing now works correctly on Dashboard load. Default San Diego location (32.7157, -117.1611) is available for all testers in dev mode.


Problem Summary โ€‹

Location spoofing did not work on initial Dashboard load due to a race condition between:

  1. App.jsx's initLocationSpoof() (runs in useEffect)
  2. Dashboard.jsx's venue loading (also runs in useEffect)

Dashboard loads venues before the spoof location is initialized, so it uses real GPS coordinates instead of the spoofed San Diego location.


Evidence from Console Logs โ€‹

Dashboard.jsx:809 ๐Ÿ”„ Starting venue load...
locationSpoof.js:102 [LocationSpoof] getLocation called - isDev: true spoofed: null  โ† BUG HERE
locationSpoof.js:49 [LocationSpoof] Using location from localStorage: {latitude: 32.7142, longitude: -117.1608}
Dashboard.jsx:815 ๐Ÿ“ User location for venues: 32.76489367499364 -117.12456377147741  โ† Used GPS instead

Timeline:

  1. App mounts โ†’ Dashboard mounts โ†’ Dashboard's useEffect fires
  2. Dashboard calls getLocation() โ†’ spoofedLocation is still null
  3. Falls through to real GPS: navigator.geolocation.getCurrentPosition()
  4. THEN App's useEffect runs โ†’ initLocationSpoof() sets spoofedLocation
  5. Too late - Dashboard already has wrong coordinates

Result:

  • Real GPS: 32.7649, -117.1246 (6-7km from seed venues)
  • All venues filtered out by 5km radius
  • Dashboard shows "No venues available"

Root Cause Analysis โ€‹

Code Flow โ€‹

App.jsx (lines 20-27):

javascript
useEffect(() => {
  async function init() {
    if (auth.currentUser) {
      await loadProfile()
    } else {
      initLocationSpoof()  // โ† Sets spoofedLocation variable
    }
  }
  init()
}, [])

locationSpoof.js (lines 41-62):

javascript
let spoofedLocation = null  // โ† Module-level variable, starts null

export function initLocationSpoof() {
  if (!isDevelopment) return

  // Check localStorage first
  const storedLocation = localStorage.getItem('dev_spoofed_location')
  if (storedLocation) {
    spoofedLocation = JSON.parse(storedLocation)
    return
  }

  // Fall back to default San Diego location
  spoofedLocation = DEFAULT_TEST_LOCATION
}

locationSpoof.js (lines 102-107):

javascript
export function getLocation(successCallback, errorCallback, options = {}) {
  console.log('[LocationSpoof] getLocation called - isDev:', isDevelopment, 'spoofed:', spoofedLocation)
  
  if (isDevelopment && spoofedLocation) {  // โ† Fails because spoofedLocation is null
    console.log('[LocationSpoof] โœ… Returning spoofed location:', spoofedLocation)
    // ... return spoofed coords
  }
  
  // Falls through to real GPS
  navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)
}

Dashboard.jsx (lines 805-822):

javascript
useEffect(() => {
  async function loadVenues() {
    getLocation(async (position) => {  // โ† Called before initLocationSpoof() runs
      const { latitude, longitude } = position.coords
      const nearbyVenues = await getNearbyVenues(latitude, longitude, 5000)
      // ...
    })
  }
  loadVenues()
}, [])

Why This Happens โ€‹

React useEffect execution order is not guaranteed between parent and child components. In this case:

  • Dashboard (child) useEffect runs before App (parent) useEffect
  • Module-level spoofedLocation variable hasn't been initialized yet
  • getLocation() check fails, falls back to GPS

Attempted Solutions (All Failed) โ€‹

Attempt 1: Increase Search Radius โŒ โ€‹

What we tried: Dev mode 50km radius, production 5km Why it failed: Poor testing etiquette, not solving the root problem User feedback: "Is there literally anything else we can do besides increasing the radius?"

Attempt 2: Auto-default to San Diego โœ… (but incomplete) โ€‹

What we tried: Set DEFAULT_TEST_LOCATION in locationSpoof.js, auto-use in initLocationSpoof()Why it partially failed: Race condition still exists - Dashboard loads before init completes Status: Code is in place but not effective due to timing issue

Attempt 3: Debug Logging ๐Ÿ” โ€‹

What we tried: Added console.log to track spoofedLocation state Result: Confirmed the race condition hypothesis Console output: Shows spoofed: null when getLocation is called


Proper Solutions (To Implement Tomorrow) โ€‹

Run initLocationSpoof() before React renders anything.

Implementation:

javascript
// main.jsx - BEFORE ReactDOM.createRoot
import { initLocationSpoof } from './lib/locationSpoof'

// Initialize location spoof synchronously before React mounts
initLocationSpoof()

// Then render
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

Pros:

  • Guarantees spoof is ready before any component mounts
  • Minimal code changes
  • Works with current architecture

Cons:

  • None identified

Option 2: Async getLocation with Init Check โ€‹

Make getLocation() check if init has run, and run it if needed.

Implementation:

javascript
// locationSpoof.js
let initialized = false

export function initLocationSpoof() {
  if (initialized) return
  // ... existing code ...
  initialized = true
}

export function getLocation(successCallback, errorCallback, options = {}) {
  // Ensure init has run
  if (isDevelopment && !initialized) {
    initLocationSpoof()
  }
  
  if (isDevelopment && spoofedLocation) {
    // ... return spoof ...
  }
  
  navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)
}

Pros:

  • Defensive programming
  • Self-healing if init is skipped

Cons:

  • Slightly more complex
  • Still has sync/async timing considerations

Option 3: Context Provider (Over-engineering) โ€‹

Create LocationProvider context that guarantees init before children render.

Pros:

  • React-idiomatic
  • Clear dependency tree

Cons:

  • Significant refactor
  • Overkill for this problem

Additional Bugs Found โ€‹

Firestore Index Missing (Secondary Issue) โ€‹

[code=failed-precondition]: The query requires an index.
Create it here: https://console.firebase.google.com/v1/r/project/lantern-app-dev/firestore/indexes?create_composite=...

Query: lanterns collection where status == active && userId == <id> ordered by litAt DESC
Impact: subscribeToActiveLanterns() fails silently
Fix: Click the Firebase Console link to auto-create the index


Files Modified Today โ€‹

/src/lib/locationSpoof.js โ€‹

  • Added DEFAULT_TEST_LOCATION constant (San Diego coordinates)
  • Modified initLocationSpoof() to auto-use default if no localStorage/env location
  • Added debug logging to getLocation() to track race condition
  • Lines changed: 13-15 (constant), 56-61 (auto-default), 102 (debug log)

/src/screens/dashboard/Dashboard.jsx โ€‹

  • Reverted to 5km radius (removed environment-based logic)
  • Added extensive debug logging for venue loading
  • Lines changed: 809-823 (venue loading useEffect)

/src/components/LightLanternModal.jsx โ€‹

  • Reverted to 5km radius (removed environment-based logic)
  • Simplified venue loading logging
  • Lines changed: 102-116 (loadNearbyVenues function)

/src/lib/venueService.js โ€‹

  • No changes today (debug logging already present from previous session)

Current State Summary โ€‹

What Works โœ… โ€‹

  • Firebase connection and auth
  • Venue data loads from Firestore (8 seed venues confirmed)
  • Distance calculation (Haversine formula accurate)
  • Icon mapping for venue categories
  • Real GPS location access

What's Broken ๐Ÿ”ด โ€‹

  1. Location spoofing race condition - Primary blocker
  2. Firestore index missing - Secondary issue, doesn't affect venue loading
  3. All venues filtered out - Consequence of #1 (using GPS ~6.5km away instead of spoofed 0m)

What's Ready for Testing โณ โ€‹

  • Venue loading infrastructure complete
  • Dashboard UI ready
  • VenuePicker component ready
  • Fire badge shows lantern counts (including 0)
  • Real-time lantern subscription (once index is created)

Next Session Action Plan โ€‹

Immediate Fixes (15 min) โ€‹

  1. Move initLocationSpoof() to main.jsx before React mounts (Option 1 above)
  2. Test Dashboard loads with San Diego coordinates
  3. Verify all 8 venues appear in "Places Nearby" list
  4. Create Firestore index via console link

Testing Validation (30 min) โ€‹

  1. Confirm venues load immediately without navigation workaround
  2. Test "Light Lantern" flow end-to-end
  3. Verify lantern appears in Dashboard with fire count
  4. Test location spoof override via debug panel

Wave Testing Setup (45 min) โ€‹

  1. Light lanterns from multiple test accounts
  2. Test wave sending between users at same venue
  3. Verify real-time updates across devices
  4. Document wave interaction behavior

Developer Notes โ€‹

Location Spoof localStorage Format โ€‹

javascript
{
  "latitude": 32.7142,
  "longitude": -117.1608
}

Stored as: localStorage.getItem('dev_spoofed_location')

Seed Venue Locations (San Diego) โ€‹

All venues within ~500m of Gaslamp Quarter:

  • Brew & Co Coffee: 32.7157, -117.1611
  • Pages & Prose Bookstore: 32.7142, -117.1608
  • The Gaslamp Tavern: 32.7135, -117.1598
  • Pacific Bites Restaurant: 32.7128, -117.1605
  • Sunset Yoga Studio: 32.7145, -117.1625
  • Zen Wellness Spa: 32.7168, -117.1535
  • Urban Bowls (Merchant): 32.7095, -117.158
  • Harbor Fitness Gym: 32.7185, -117.1695

Default spoof location: 32.7157, -117.1611 (Brew & Co Coffee)
Why: Center of venue cluster, within 5km of all venues


Questions for User โ€‹

  1. Confirm Option 1 (synchronous init in main.jsx) is acceptable approach?
  2. Should we keep the 5km radius or adjust for production vs dev?
  3. Any other test accounts needed beyond current user?
  4. Target timeline for pilot testing?

References โ€‹

  • Location spoof implementation: src/lib/locationSpoof.js
  • Dashboard venue loading: src/screens/dashboard/Dashboard.jsx (lines 805-854)
  • Venue service: src/lib/venueService.js
  • Environment setup docs: docs/engineering/ENVIRONMENT_SETUP.md
  • Local testing guide: docs/engineering/LOCAL_TESTING.md

End of Session Notes
Created by: GitHub Copilot
Session Date: January 5, 2026
Next Session: Fix race condition via main.jsx init, then proceed with wave testing

Built with VitePress