Skip to content

Commit 5b5c76c

Browse files
authored
feat: Add geo-blocking analytics tracking (MetaMask#22126)
# Geoblock Analytics Implementation ## Overview This PR implements analytics tracking for geoblocking events in Predict, allowing us to understand when and where users are blocked from performing actions due to geographic restrictions. CHANGELOG entry: null ## Changes ### 1. State Structure Refactoring **Consolidated eligibility and geoblock data into a single nested structure:** ```typescript // Before eligibility: { [key: string]: boolean } geoBlockData: { [key: string]: { country?: string } } // After eligibility: { [key: string]: { eligible: boolean; country?: string; } } ``` **Benefits:** - Simplified state management - one source of truth - Atomic updates - eligible status and country always in sync - Easier to consume - no need to access two separate state objects ### 2. Analytics Event Implementation **Event:** `PREDICT_GEO_BLOCKED_TRIGGERED` **Properties:** - `country` - The country code where the user is located (from geoblock API) - `attempted_action` - The action the user tried to perform **Attempted Actions:** - `deposit` - User tried to add funds - `predict_action` - User tried to place a prediction - `cashout` - User tried to cash out a position - `claim` - User tried to claim winnings - `withdraw` - User tried to withdraw funds (not currently guarded) ### 3. Integration Points **Guarded Actions:** All actions that require eligibility checks use `executeGuardedAction` from `usePredictActionGuard` hook: ```typescript executeGuardedAction( () => { // Action logic }, { attemptedAction: PredictEventValues.ATTEMPTED_ACTION.DEPOSIT } ); ``` **Components Updated:** - ✅ `PredictBalance.tsx` - Add funds (deposit) - ✅ `PredictAddFundsSheet.tsx` - Add funds (deposit) - ✅ `PredictMarketDetails.tsx` - Buy (predict), Claim - ✅ `PredictPositionsHeader.tsx` - Claim - ✅ `PredictPositionDetail.tsx` - Cash out - ✅ `PredictMarketSingle.tsx` - Buy (predict) - ✅ `PredictMarketOutcome.tsx` - Buy (predict) - ✅ `PredictMarketMultiple.tsx` - Buy (predict) ### 4. Controller Updates **`PredictController.ts`:** - `refreshEligibility()` - Now stores both `eligible` and `country` together - `trackGeoBlockedEvent()` - New method to track analytics when user is blocked - Updated to use consolidated eligibility state **`usePredictEligibility.ts` hook:** - Now returns both `isEligible` and `country` - Simplifies component consumption ### 5. Testing **All tests updated and passing:** - ✅ PredictController tests (1687 tests) - ✅ usePredictEligibility tests - ✅ Component tests updated for new hook signature - ✅ Test suites: 67 passed ## Data Flow ``` User Action Attempt ↓ executeGuardedAction ↓ Check eligibility (from state) ↓ Blocked? → Yes → trackGeoBlockedEvent(country, attemptedAction) ↓ ↓ No Analytics Event Sent ↓ Execute Action ``` ## Analytics Dashboard **To query geoblock events in the analytics dashboard:** ```sql SELECT country, attempted_action, COUNT(*) as blocked_count, DATE(timestamp) as date FROM events WHERE event_name = 'PREDICT_GEO_BLOCKED_TRIGGERED' GROUP BY country, attempted_action, DATE(timestamp) ORDER BY blocked_count DESC ``` **Key Metrics:** - Most blocked countries - Most common blocked actions - Trends over time - Conversion rates by geography ## Example Event ```json { "event": "PREDICT_GEO_BLOCKED_TRIGGERED", "properties": { "country": "US", "attempted_action": "deposit" }, "timestamp": "2024-01-15T10:30:00.000Z" } ``` ## Files Changed ### Core Implementation - `app/components/UI/Predict/controllers/PredictController.ts` - `app/components/UI/Predict/hooks/usePredictEligibility.ts` - `app/components/UI/Predict/hooks/usePredictActionGuard.ts` - `app/components/UI/Predict/constants/eventNames.ts` ### Components - `app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx` - `app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.tsx` - `app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.tsx` - `app/components/UI/Predict/components/PredictPositionsHeader/PredictPositionsHeader.tsx` - `app/components/UI/Predict/components/PredictPositionDetail/PredictPositionDetail.tsx` - `app/components/UI/Predict/components/PredictMarketSingle/PredictMarketSingle.tsx` - `app/components/UI/Predict/components/PredictMarketOutcome/PredictMarketOutcome.tsx` - `app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.tsx` ### Tests - `app/components/UI/Predict/controllers/PredictController.test.ts` - `app/components/UI/Predict/hooks/usePredictEligibility.test.ts` - `app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.test.tsx` ## Testing Instructions ### Manual Testing 1. **Setup geoblock scenario:** - Use VPN or modify geoblock API response to return `isEligible: false` 2. **Test each action:** - Try to deposit funds → Check analytics event sent - Try to place a prediction → Check analytics event sent - Try to cash out → Check analytics event sent - Try to claim winnings → Check analytics event sent 3. **Verify analytics:** - Open DevTools console - Look for `📊 [Analytics] PREDICT_GEO_BLOCKED_TRIGGERED` logs - Verify `country` and `attempted_action` are correct ### Automated Testing ```bash # Run all Predict tests yarn jest ./app/components/UI/Predict # Run specific test suites yarn jest app/components/UI/Predict/controllers/PredictController.test.ts yarn jest app/components/UI/Predict/hooks/usePredictEligibility.test.ts ``` ## Migration Notes ### State Migration No migration required. The consolidated state structure is backward compatible: - Old state with empty `eligibility` object will work correctly - `refreshEligibility()` will populate new structure on first call ### API Compatibility No API changes required. The geoblock API response format remains the same: ```typescript interface GeoBlockResponse { isEligible: boolean; country?: string; } ``` ## Future Enhancements 1. **Enhanced Metrics:** - Add `ip` to analytics (currently stored but not tracked) - Track `region` for more granular insights 2. **User Education:** - Show country-specific messaging - Suggest alternative actions for blocked users 3. **Dashboard Integration:** - Real-time geoblock monitoring - Alerts for sudden spikes in blocked regions ## Checklist - [x] State structure refactored and tested - [x] Analytics event implemented - [x] All guarded actions updated with `attemptedAction` - [x] Tests updated and passing (67 suites, 1687 tests) - [x] Dev logging added for debugging - [x] Code follows project guidelines - [x] No breaking changes ## Related Issues - Tracking user geoblocking for compliance and UX insights - Understanding geographic distribution of blocked actions <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Add geo-block analytics and migrate eligibility to include country, wiring attemptedAction tracking across guarded Predict actions. > > - **Analytics**: > - Add `PREDICT_GEO_BLOCKED_TRIGGERED` event and properties `country` and `attempted_action`. > - **State/Controller**: > - Change `PredictController.state.eligibility` to `{ [providerId]: { eligible: boolean; country?: string } }`. > - Update `refreshEligibility()` to store country; add `trackGeoBlockTriggered()`. > - **Providers**: > - Update `PredictProvider.isEligible()` to return `{ isEligible, country }` via `GeoBlockResponse`. > - Implement in `PolymarketProvider.isEligible()`. > - **Hooks**: > - `usePredictEligibility()` returns `{ isEligible, country }` (default `false` when unset). > - `usePredictActionGuard()` accepts `{ checkBalance, attemptedAction }` and tracks geo-blocks when blocked. > - **UI (guarded actions instrumented with `attemptedAction`)**: > - `PredictAddFundsSheet`, `PredictBalance` → deposit. > - `PredictMarketDetails`, `PredictMarketSingle`, `PredictMarketOutcome`, `PredictMarketMultiple` → predict/buy, claim. > - `PredictPositionsHeader` → claim; `PredictPositionDetail` → cashout. > - **MetaMetrics**: > - Register new event in `MetaMetrics.events`. > - **Tests**: > - Update controller, hooks, provider, and component tests for new eligibility shape, geo-block tracking, and attemptedAction args. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e6d0566. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8f960c8 commit 5b5c76c

23 files changed

Lines changed: 243 additions & 74 deletions

app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ describe('PredictAddFundsSheet', () => {
372372
expect(mockExecuteGuardedAction).toHaveBeenCalledTimes(1);
373373
expect(mockExecuteGuardedAction).toHaveBeenCalledWith(
374374
expect.any(Function),
375+
{ attemptedAction: 'deposit' },
375376
);
376377
});
377378

app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { usePredictActionGuard } from '../../hooks/usePredictActionGuard';
1919
import { POLYMARKET_PROVIDER_ID } from '../../providers/polymarket/constants';
2020
import { NavigationProp, useNavigation } from '@react-navigation/native';
2121
import { PredictNavigationParamList } from '../../types/navigation';
22+
import { PredictEventValues } from '../../constants/eventNames';
2223
import {
2324
usePredictBottomSheet,
2425
type PredictBottomSheetRef,
@@ -50,9 +51,12 @@ const PredictAddFundsSheet = forwardRef<
5051
};
5152

5253
const handleAddFunds = () => {
53-
executeGuardedAction(() => {
54-
deposit();
55-
});
54+
executeGuardedAction(
55+
() => {
56+
deposit();
57+
},
58+
{ attemptedAction: PredictEventValues.ATTEMPTED_ACTION.DEPOSIT },
59+
);
5660
};
5761

5862
useImperativeHandle(ref, getRefHandlers, [getRefHandlers]);

app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { usePredictActionGuard } from '../../hooks/usePredictActionGuard';
3535
import { NavigationProp, useNavigation } from '@react-navigation/native';
3636
import { PredictNavigationParamList } from '../../types/navigation';
3737
import { usePredictWithdraw } from '../../hooks/usePredictWithdraw';
38+
import { PredictEventValues } from '../../constants/eventNames';
3839

3940
// This is a temporary component that will be removed when the deposit flow is fully implemented
4041
interface PredictBalanceProps {
@@ -68,9 +69,12 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({ onLayout }) => {
6869
}, [isDepositPending, loadBalance]);
6970

7071
const handleAddFunds = useCallback(() => {
71-
executeGuardedAction(() => {
72-
deposit();
73-
});
72+
executeGuardedAction(
73+
() => {
74+
deposit();
75+
},
76+
{ attemptedAction: PredictEventValues.ATTEMPTED_ACTION.DEPOSIT },
77+
);
7478
}, [deposit, executeGuardedAction]);
7579

7680
const handleWithdraw = useCallback(() => {

app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import { PredictMarket, Recurrence } from '../../types';
88
import { PredictEventValues } from '../../constants/eventNames';
99
import Routes from '../../../../../constants/navigation/Routes';
1010

11+
jest.mock('../../../../../core/Engine', () => ({
12+
context: {
13+
PredictController: {
14+
trackGeoBlockTriggered: jest.fn(),
15+
},
16+
},
17+
}));
18+
1119
const mockNavigate = jest.fn();
1220
jest.mock('@react-navigation/native', () => {
1321
const actualNav = jest.requireActual('@react-navigation/native');

app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ const PredictMarketMultiple: React.FC<PredictMarketMultipleProps> = ({
132132
},
133133
});
134134
},
135-
{ checkBalance: true },
135+
{
136+
checkBalance: true,
137+
attemptedAction: PredictEventValues.ATTEMPTED_ACTION.PREDICT,
138+
},
136139
);
137140
};
138141

app/components/UI/Predict/components/PredictMarketOutcome/PredictMarketOutcome.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import PredictMarketOutcome from '.';
1010
const mockAlert = jest.fn();
1111
jest.spyOn(Alert, 'alert').mockImplementation(mockAlert);
1212

13+
jest.mock('../../../../../core/Engine', () => ({
14+
context: {
15+
PredictController: {
16+
trackGeoBlockTriggered: jest.fn(),
17+
},
18+
},
19+
}));
20+
1321
const mockNavigate = jest.fn();
1422
jest.mock('@react-navigation/native', () => {
1523
const actualNav = jest.requireActual('@react-navigation/native');

app/components/UI/Predict/components/PredictMarketOutcome/PredictMarketOutcome.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ const PredictMarketOutcome: React.FC<PredictMarketOutcomeProps> = ({
9696
},
9797
});
9898
},
99-
{ checkBalance: true },
99+
{
100+
checkBalance: true,
101+
attemptedAction: PredictEventValues.ATTEMPTED_ACTION.PREDICT,
102+
},
100103
);
101104
};
102105

app/components/UI/Predict/components/PredictMarketSingle/PredictMarketSingle.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import Routes from '../../../../../constants/navigation/Routes';
1616
const mockAlert = jest.fn();
1717
jest.spyOn(Alert, 'alert').mockImplementation(mockAlert);
1818

19+
jest.mock('../../../../../core/Engine', () => ({
20+
context: {
21+
PredictController: {
22+
trackGeoBlockTriggered: jest.fn(),
23+
},
24+
},
25+
}));
26+
1927
// Mock navigation
2028
const mockNavigate = jest.fn();
2129
jest.mock('@react-navigation/native', () => ({

app/components/UI/Predict/components/PredictMarketSingle/PredictMarketSingle.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ const PredictMarketSingle: React.FC<PredictMarketSingleProps> = ({
176176
},
177177
});
178178
},
179-
{ checkBalance: true },
179+
{
180+
checkBalance: true,
181+
attemptedAction: PredictEventValues.ATTEMPTED_ACTION.PREDICT,
182+
},
180183
);
181184
};
182185

app/components/UI/Predict/components/PredictPositionDetail/PredictPositionDetail.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,23 @@ const PredictPosition: React.FC<PredictPositionProps> = ({
6060
)?.groupItemTitle;
6161

6262
const onCashOut = () => {
63-
executeGuardedAction(() => {
64-
const _outcome = market?.outcomes.find(
65-
(o) => o.id === position.outcomeId,
66-
);
67-
navigate(Routes.PREDICT.MODALS.ROOT, {
68-
screen: Routes.PREDICT.MODALS.SELL_PREVIEW,
69-
params: {
70-
market,
71-
position,
72-
outcome: _outcome,
73-
entryPoint: PredictEventValues.ENTRY_POINT.PREDICT_MARKET_DETAILS,
74-
},
75-
});
76-
});
63+
executeGuardedAction(
64+
() => {
65+
const _outcome = market?.outcomes.find(
66+
(o) => o.id === position.outcomeId,
67+
);
68+
navigate(Routes.PREDICT.MODALS.ROOT, {
69+
screen: Routes.PREDICT.MODALS.SELL_PREVIEW,
70+
params: {
71+
market,
72+
position,
73+
outcome: _outcome,
74+
entryPoint: PredictEventValues.ENTRY_POINT.PREDICT_MARKET_DETAILS,
75+
},
76+
});
77+
},
78+
{ attemptedAction: PredictEventValues.ATTEMPTED_ACTION.CASHOUT },
79+
);
7780
};
7881

7982
const renderValueText = () => {

0 commit comments

Comments
 (0)