feat(perps): consume perps controller analytics contract in MetaMask Mobile#33095
Conversation
Wire Mobile to the perps-controller analytics contract: register new MetaMetrics events, propagate entry/discovery/UTM and fee fields into trackingData, emit controller search events, and remove duplicate client-side trade transaction emissions. Co-authored-by: Cursor <cursoragent@cursor.com>
Document why deeplink UTM attribution failures are swallowed so navigation continues when Engine/controller is unavailable. Co-authored-by: Cursor <cursoragent@cursor.com>
…002) Pass market-list source_section through market details into order route params and toPerpsEntryAttribution so trade trackingData includes discoverySource fields. Co-authored-by: Cursor <cursoragent@cursor.com>
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
Automated dev run — MANUAL-000002
Worker reportDescriptionConsume the TAT-3463 ChangelogCHANGELOG entry: null Related issuesFixes: https://consensyssoftware.atlassian.net/browse/TAT-3463 Manual testing stepsFeature: perps controller analytics contract consumption
Scenario: order and search analytics use controller contract
Given the app is running with @metamask/perps-controller 9.2.1
And MetaMetrics is enabled
When the user opens Perps markets and searches for a symbol
Then Perp Search Query / Result Tapped / Abandoned events can fire from the market list
When the user places, closes, cancels, flips, or updates TP/SL
Then trackingData includes entryPoint/discovery fields and fee fields where available
And the client does not emit a second Perp Trade Transaction for the same controller eventN/A for visual UI walkthrough — analytics-only change; validated via recipe state assertions + unit tests. Screenshots/RecordingsN/A — analytics/contract wiring only; no visual UI change. Proof is recipe state assertions and unit tests (see Validation Logs). BeforeN/A AfterN/A Validation Reciperecipe.json (17 steps — state asserts for AC1–AC5 analytics contract consumption){
"schema_version": 1,
"title": "Consume perps controller analytics contract (MANUAL-000002)",
"description": "Prove Mobile consumes TAT-3463 perps-controller analytics contract: package version, MetaMetrics event registration, attribution wiring, search events, and no duplicate client trade-transaction emissions.",
"validate": {
"workflow": {
"entry": "ac1-package-version",
"nodes": {
"ac1-package-version": {
"action": "command",
"intent": "AC1: confirm installed @metamask/perps-controller includes core#9311 contract",
"proofTarget": "ac1",
"cmd": "node -e \"const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('node_modules/@metamask/perps-controller/package.json','utf8')); const cl=fs.readFileSync('node_modules/@metamask/perps-controller/CHANGELOG.md','utf8'); if (!pkg.version.startsWith('9.')) { console.error('unexpected version', pkg.version); process.exit(1);} if (!cl.includes('#9311') && !cl.includes('PerpsAttributionContext')) { console.error('changelog missing TAT-3463 contract markers'); process.exit(1);} console.log('ok', pkg.version);\"",
"next": "ac1-assert-output"
},
"ac1-assert-output": {
"action": "assert_output",
"intent": "AC1: package version command printed ok",
"proofTarget": "ac1",
"source": "ac1-package-version",
"stream": "stdout",
"contains": "ok",
"next": "ac2-search-query"
},
"ac2-search-query": {
"action": "assert_file",
"intent": "AC2: MetaMetrics registers Perp Search Query",
"proofTarget": "ac2",
"path": "app/core/Analytics/MetaMetrics.events.ts",
"contains": "Perp Search Query",
"next": "ac2-search-result"
},
"ac2-search-result": {
"action": "assert_file",
"intent": "AC2: MetaMetrics registers Perp Search Result Tapped",
"proofTarget": "ac2",
"path": "app/core/Analytics/MetaMetrics.events.ts",
"contains": "Perp Search Result Tapped",
"next": "ac2-search-abandoned"
},
"ac2-search-abandoned": {
"action": "assert_file",
"intent": "AC2: MetaMetrics registers Perp Search Abandoned",
"proofTarget": "ac2",
"path": "app/core/Analytics/MetaMetrics.events.ts",
"contains": "Perp Search Abandoned",
"next": "ac2-considered"
},
"ac2-considered": {
"action": "assert_file",
"intent": "AC2: MetaMetrics registers Perp Transaction Considered",
"proofTarget": "ac2",
"path": "app/core/Analytics/MetaMetrics.events.ts",
"contains": "Perp Transaction Considered",
"next": "ac2-quote"
},
"ac2-quote": {
"action": "assert_file",
"intent": "AC2: MetaMetrics registers Perp Trade Quote Received",
"proofTarget": "ac2",
"path": "app/core/Analytics/MetaMetrics.events.ts",
"contains": "Perp Trade Quote Received",
"next": "ac3-attribution-helper"
},
"ac3-attribution-helper": {
"action": "assert_file",
"intent": "AC3: attribution helper maps entryPoint",
"proofTarget": "ac3",
"path": "app/components/UI/Perps/utils/perpsAnalyticsAttribution.ts",
"contains": "entryPoint",
"next": "ac3-deeplink-utm"
},
"ac3-deeplink-utm": {
"action": "assert_file",
"intent": "AC3: deeplink handler applies UTM attribution context",
"proofTarget": "ac3",
"path": "app/core/DeeplinkManager/handlers/legacy/handlePerpsUrl.ts",
"contains": "setPerpsUtmAttribution",
"next": "ac3-order-entrypoint"
},
"ac3-order-entrypoint": {
"action": "assert_file",
"intent": "AC3: order ticket wires entry attribution helper into trackingData",
"proofTarget": "ac3",
"path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx",
"contains": "toPerpsEntryAttribution",
"next": "ac3-order-hl-fee"
},
"ac3-order-hl-fee": {
"action": "assert_file",
"intent": "AC3: order ticket trackingData includes hlFeeRate",
"proofTarget": "ac3",
"path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx",
"contains": "hlFeeRate",
"next": "ac3-close-entrypoint"
},
"ac3-close-entrypoint": {
"action": "assert_file",
"intent": "AC3: close position wires entry attribution helper into trackingData",
"proofTarget": "ac3",
"path": "app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx",
"contains": "toPerpsEntryAttribution",
"next": "ac4-search-event"
},
"ac4-search-event": {
"action": "assert_file",
"intent": "AC4: market list emits controller Search Query event",
"proofTarget": "ac4",
"path": "app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx",
"contains": "PERPS_SEARCH_QUERY",
"next": "ac5-no-client-trade-dup"
},
"ac5-no-client-trade-dup": {
"action": "command",
"intent": "AC5: usePerpsOrderExecution must not emit client-side PERPS_TRADE_TRANSACTION",
"proofTarget": "ac5",
"cmd": "node -e \"const fs=require('fs'); const src=fs.readFileSync('app/components/UI/Perps/hooks/usePerpsOrderExecution.ts','utf8'); if (/track\\\\(\\\\s*MetaMetricsEvents\\\\.PERPS_TRADE_TRANSACTION/.test(src)) { console.error('duplicate client PERPS_TRADE_TRANSACTION still present'); process.exit(1);} console.log('no-client-trade-dup-ok');\"",
"next": "ac5-assert-output"
},
"ac5-assert-output": {
"action": "assert_output",
"intent": "AC5: confirm no client trade-transaction duplicate emission",
"proofTarget": "ac5",
"source": "ac5-no-client-trade-dup",
"stream": "stdout",
"contains": "no-client-trade-dup-ok",
"next": "ac3-flip-metamask-fee"
},
"ac3-flip-metamask-fee": {
"action": "assert_file",
"intent": "AC3: flip sheet passes metamaskFee for controller flip analytics",
"proofTarget": "ac3",
"path": "app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx",
"contains": "metamaskFee",
"next": "done"
},
"done": {
"action": "end",
"status": "pass",
"intent": "All analytics contract consumption assertions passed"
}
}
}
}
}Validation LogsCommand: "$RUNNER_CMD" run temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe.json --adapter mobile --artifacts-dir temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run --project-root /Users/deeeed/dev/metamask/metamask-mobile-1 --slot macwork-mmdev-1 --jsonFull output (17/17 passed)Pre-merge author checklist
Performance checks (if applicable)
Pre-merge reviewer checklist
Self-review fix noteForwarded market-list |
…nsume-perps-controller-analy Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # app/components/UI/Perps/hooks/usePerpsOrderExecution.test.ts # app/components/UI/Perps/hooks/usePerpsOrderExecution.ts
Cancel flows now include totalFee and marketPrice so TrackingData matches the controller contract after merging main. Co-authored-by: Cursor <cursoragent@cursor.com>
geositta
left a comment
There was a problem hiding this comment.
Requesting changes for Two search correctness defects: suggested watchlist results produce inaccurate tap metrics, and unresolved result counts are recorded as zero during abandonment.
| } else if (trimmedQuery) { | ||
| source_section = PERPS_EVENT_VALUE.SOURCE_SECTION.ACTIVE_SEARCH; | ||
| const resultRank = | ||
| filteredMarkets.findIndex((m) => m.symbol === market.symbol) + 1; |
There was a problem hiding this comment.
Please derive resultRank and results_count from the same collection rendered to the user.
In watchlist mode, the UI combines matching watchlist markets with matching suggestions, while filteredMarkets excludes suggestions.
Tapping a suggested result therefore omits result_rank and reports an incorrect result count. A shared visibleSearchResults collection would keep rendering and analytics consistent.
| : 'browse'; | ||
|
|
||
| lastEmittedSearchQueryRef.current = normalizedQuery; | ||
| lastEmittedSearchResultsCountRef.current = resultsSettled ? resultCount : 0; |
There was a problem hiding this comment.
The stale count bugbot issue in the if statement below here is resolved, but resetting this value to 0 records "no results" when results are still loading. Please store undefined when resultsSettled=false and omit results_count from PERPS_SEARCH_ABANDONED, matching PERPS_SEARCH_QUERY.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: SmokePerps (primary): The PR touches nearly every Perps view and hook:
SmokeWalletPlatform: Per tag description, Perps is a section inside the Trending tab. Changes to PerpsMarketListView, PerpsMarketDetailsView, and other Perps views that are embedded in Trending affect this tag. SmokeConfirmations: Changes to pay-with-modal.tsx and usePayWithCryptoSection.ts (both in the confirmations directory) add markPerpsPaymentTokenSelection() calls. These files are part of the confirmation pay-with flow used during Perps deposit-and-order transactions. The usePayWithPerpsSection hook also changed. These are functional changes to the confirmation flow, not just analytics. Performance tests: The @PerformancePreps tag covers perps market loading, position management, add funds flow, and order execution. The PerpsOrderView and PerpsMarketListView changes include new useEffect hooks with debounced timers and memoized computations that could affect render performance. However, the changes are primarily analytics instrumentation (tracking events, refs, debounced timers) rather than core rendering path changes. The risk of performance regression is low but non-zero given the added complexity in PerpsOrderView and PerpsMarketListView. Performance Test Selection: |
|
⚡ Performance Test Results
✅ All tests passed · 2 tests · 1 device 📱 Devices tested (1)Android: Google Pixel 8 Pro (v14.0) ✅ Passed Tests (2)
Branch: |



Description
Adopts the controller-owned perps analytics contract and completes the assigned tickets.
Beyond the initial contract adoption (register the five controller MetaMetrics events, entry/discovery/UTM/
hlFeeRate/metamaskFeetrackingData on the order lifecycle, controller search events from the market list, removal of duplicate client-sidePERPS_TRADE_TRANSACTIONemissions so submitted/terminal come only from the controller), this now also wires:PERPS_SCREEN_VIEWED()sort_applied/filter_appliedincl. the watchlist filterplace_orderbutton for ALL users incl. the deposit-required pathabandon_orderon genuine exit — back/swipe/hardware-back and tab-switch-away, suppressed on internal TP/SL / payment-selector nav and after placementscreen_type=add_margin/remove_marginon margin modal openwatchlistedon asset_detail onlyPERPS_TRANSACTION_CONSIDERED+PERPS_TRADE_QUOTE_RECEIVEDnow emittedNo SDK bump or patch required on mobile — Metro uses the correct ESM build. The published 9.2.1 CommonJS build bug is core-side and fixed in perps-controller 9.2.2 (MetaMask/core#9471; upstream nktkas/hyperliquid#165).
Deferred
order_execution_latency_mswiring lands with the 9.2.2 bump (newTrackingData.orderExecutionLatencyMs).Changelog
CHANGELOG entry: null
Related issues
Refs: MetaMask/core#9311
Manual testing steps
N/A for visual UI walkthrough — analytics-only change; validated via recipe state assertions + unit tests.
Screenshots/Recordings
Analytics-only change; no screenshots or video. Proof is recipe state assertions and unit tests in Validation Logs.
Validation Recipe
recipe.json (17 steps — state asserts for AC1–AC5 analytics contract consumption)
{ "schema_version": 1, "title": "Consume perps controller analytics contract (MANUAL-000002)", "description": "Prove Mobile consumes perps-controller analytics contract: package version, MetaMetrics event registration, attribution wiring, search events, and no duplicate client trade-transaction emissions.", "validate": { "workflow": { "entry": "ac1-package-version", "nodes": { "ac1-package-version": { "action": "command", "intent": "AC1: confirm installed @metamask/perps-controller includes core#9311 contract", "proofTarget": "ac1", "cmd": "node -e \"const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('node_modules/@metamask/perps-controller/package.json','utf8')); const cl=fs.readFileSync('node_modules/@metamask/perps-controller/CHANGELOG.md','utf8'); if (!pkg.version.startsWith('9.')) { console.error('unexpected version', pkg.version); process.exit(1);} if (!cl.includes('#9311') && !cl.includes('PerpsAttributionContext')) { console.error('changelog missing contract markers'); process.exit(1);} console.log('ok', pkg.version);\"", "next": "ac1-assert-output" }, "ac1-assert-output": { "action": "assert_output", "intent": "AC1: package version command printed ok", "proofTarget": "ac1", "source": "ac1-package-version", "stream": "stdout", "contains": "ok", "next": "ac2-search-query" }, "ac2-search-query": { "action": "assert_file", "intent": "AC2: MetaMetrics registers Perp Search Query", "proofTarget": "ac2", "path": "app/core/Analytics/MetaMetrics.events.ts", "contains": "Perp Search Query", "next": "ac2-search-result" }, "ac2-search-result": { "action": "assert_file", "intent": "AC2: MetaMetrics registers Perp Search Result Tapped", "proofTarget": "ac2", "path": "app/core/Analytics/MetaMetrics.events.ts", "contains": "Perp Search Result Tapped", "next": "ac2-search-abandoned" }, "ac2-search-abandoned": { "action": "assert_file", "intent": "AC2: MetaMetrics registers Perp Search Abandoned", "proofTarget": "ac2", "path": "app/core/Analytics/MetaMetrics.events.ts", "contains": "Perp Search Abandoned", "next": "ac2-considered" }, "ac2-considered": { "action": "assert_file", "intent": "AC2: MetaMetrics registers Perp Transaction Considered", "proofTarget": "ac2", "path": "app/core/Analytics/MetaMetrics.events.ts", "contains": "Perp Transaction Considered", "next": "ac2-quote" }, "ac2-quote": { "action": "assert_file", "intent": "AC2: MetaMetrics registers Perp Trade Quote Received", "proofTarget": "ac2", "path": "app/core/Analytics/MetaMetrics.events.ts", "contains": "Perp Trade Quote Received", "next": "ac3-attribution-helper" }, "ac3-attribution-helper": { "action": "assert_file", "intent": "AC3: attribution helper maps entryPoint", "proofTarget": "ac3", "path": "app/components/UI/Perps/utils/perpsAnalyticsAttribution.ts", "contains": "entryPoint", "next": "ac3-deeplink-utm" }, "ac3-deeplink-utm": { "action": "assert_file", "intent": "AC3: deeplink handler applies UTM attribution context", "proofTarget": "ac3", "path": "app/core/DeeplinkManager/handlers/legacy/handlePerpsUrl.ts", "contains": "setPerpsUtmAttribution", "next": "ac3-order-entrypoint" }, "ac3-order-entrypoint": { "action": "assert_file", "intent": "AC3: order ticket wires entry attribution helper into trackingData", "proofTarget": "ac3", "path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx", "contains": "toPerpsEntryAttribution", "next": "ac3-order-hl-fee" }, "ac3-order-hl-fee": { "action": "assert_file", "intent": "AC3: order ticket trackingData includes hlFeeRate", "proofTarget": "ac3", "path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx", "contains": "hlFeeRate", "next": "ac3-close-entrypoint" }, "ac3-close-entrypoint": { "action": "assert_file", "intent": "AC3: close position wires entry attribution helper into trackingData", "proofTarget": "ac3", "path": "app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx", "contains": "toPerpsEntryAttribution", "next": "ac4-search-event" }, "ac4-search-event": { "action": "assert_file", "intent": "AC4: market list emits controller Search Query event", "proofTarget": "ac4", "path": "app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx", "contains": "PERPS_SEARCH_QUERY", "next": "ac5-no-client-trade-dup" }, "ac5-no-client-trade-dup": { "action": "command", "intent": "AC5: usePerpsOrderExecution must not emit client-side PERPS_TRADE_TRANSACTION", "proofTarget": "ac5", "cmd": "node -e \"const fs=require('fs'); const src=fs.readFileSync('app/components/UI/Perps/hooks/usePerpsOrderExecution.ts','utf8'); if (/track\\\\(\\\\s*MetaMetricsEvents\\\\.PERPS_TRADE_TRANSACTION/.test(src)) { console.error('duplicate client PERPS_TRADE_TRANSACTION still present'); process.exit(1);} console.log('no-client-trade-dup-ok');\"", "next": "ac5-assert-output" }, "ac5-assert-output": { "action": "assert_output", "intent": "AC5: confirm no client trade-transaction duplicate emission", "proofTarget": "ac5", "source": "ac5-no-client-trade-dup", "stream": "stdout", "contains": "no-client-trade-dup-ok", "next": "ac3-flip-metamask-fee" }, "ac3-flip-metamask-fee": { "action": "assert_file", "intent": "AC3: flip sheet passes metamaskFee for controller flip analytics", "proofTarget": "ac3", "path": "app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx", "contains": "metamaskFee", "next": "done" }, "done": { "action": "end", "status": "pass", "intent": "All analytics contract consumption assertions passed" } } } } }Validation Logs
Command:
Full output (17/17 passed)
Pre-merge author checklist
Performance checks (if applicable)
Pre-merge reviewer checklist
Self-review fix note
Forwarded market-list
source_sectionthrough market details into ordertrackingDatadiscovery fields; added unit coverage for the handoff.Note
Medium Risk
Large analytics surface across trade, search, and navigation with abandon/suppression logic; wrong events could skew product metrics but does not change order execution or funds handling.
Overview
Aligns MetaMask Mobile Perps with the controller-owned analytics contract: trade lifecycle events (
PERPS_TRADE_TRANSACTION, partial fills) are no longer emitted fromusePerpsOrderExecution—onlytrackingDatais forwarded so the controller owns submitted/terminal events.Attribution & trackingData — Adds
toPerpsEntryAttribution/ UTM helpers and threadsentryPoint, discovery (source_section),hlFeeRate, and fee fields through order, close, cancel, flip, TP/SL, and margin flows. UTM is merged into clientPERPS_SCREEN_VIEWEDviausePerpsEventTracking.New client events & funnels — Market list: debounced
PERPS_SEARCH_QUERY, result taps, abandonment, sort/filter applied, and search screen views. Order screen: debouncedPERPS_TRANSACTION_CONSIDERED, pay-tokenPERPS_TRADE_QUOTE_RECEIVED, place-order tap (all paths), andusePerpsAbandonOrderTrackingforabandon_orderon real exits (not child routes or after commit). Close flow gets the same abandon hook plus routebuttonClicked/buttonLocationon screen views. Adjust margin screen views by mode; asset details addswatchlistedand forwardssource_sectioninto orders.Supporting pieces —
derivePerpsTradeActionfor considered vs executed action parity; payment-token selector dismissed tracking; navigation params extended for close entry CTAs andsource_section.Reviewed by Cursor Bugbot for commit 6c19369. Bugbot is set up for automated code reviews on this repo. Configure here.