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:
- src/main.jsx - Added synchronous call to
initLocationSpoof()before React renders - 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:
- App.jsx's
initLocationSpoof()(runs in useEffect) - 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 insteadTimeline:
- App mounts โ Dashboard mounts โ Dashboard's useEffect fires
- Dashboard calls
getLocation()โspoofedLocationis stillnull - Falls through to real GPS:
navigator.geolocation.getCurrentPosition() - THEN App's useEffect runs โ
initLocationSpoof()setsspoofedLocation - 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):
useEffect(() => {
async function init() {
if (auth.currentUser) {
await loadProfile()
} else {
initLocationSpoof() // โ Sets spoofedLocation variable
}
}
init()
}, [])locationSpoof.js (lines 41-62):
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):
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):
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
spoofedLocationvariable 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) โ
Option 1: Synchronous Init (Recommended) โ
Run initLocationSpoof() before React renders anything.
Implementation:
// 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:
// 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_LOCATIONconstant (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 ๐ด โ
- Location spoofing race condition - Primary blocker
- Firestore index missing - Secondary issue, doesn't affect venue loading
- 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) โ
- Move
initLocationSpoof()tomain.jsxbefore React mounts (Option 1 above) - Test Dashboard loads with San Diego coordinates
- Verify all 8 venues appear in "Places Nearby" list
- Create Firestore index via console link
Testing Validation (30 min) โ
- Confirm venues load immediately without navigation workaround
- Test "Light Lantern" flow end-to-end
- Verify lantern appears in Dashboard with fire count
- Test location spoof override via debug panel
Wave Testing Setup (45 min) โ
- Light lanterns from multiple test accounts
- Test wave sending between users at same venue
- Verify real-time updates across devices
- Document wave interaction behavior
Developer Notes โ
Location Spoof localStorage Format โ
{
"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 โ
- Confirm Option 1 (synchronous init in main.jsx) is acceptable approach?
- Should we keep the 5km radius or adjust for production vs dev?
- Any other test accounts needed beyond current user?
- 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