diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index 75c648d587..20a6b08251 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add the optional `PerpsPerformance.onControllerConstructed` post-hydration timestamp hook ([#9906](https://github.com/MetaMask/core/pull/9906)) +- Add an explicit trace ID overload to `PerpsTracer.setMeasurement`, allowing clients to target preload measurements to their named trace ([#9906](https://github.com/MetaMask/core/pull/9906)) - Add `PERPS_EVENT_PROPERTY.PREVIOUS_LEVERAGE` (`previous_leverage`) for Perp UI Interaction `leverage_changed` events so clients can import the Segment property key from `@metamask/perps-controller` instead of a local interim constant ([#9881](https://github.com/MetaMask/core/pull/9881)) +### Changed + +- Target market and user preload measurements to their named traces, and omit wallet addresses from user-preload trace data ([#9906](https://github.com/MetaMask/core/pull/9906)) + ## [12.0.0] ### Added diff --git a/packages/perps-controller/src/PerpsController.ts b/packages/perps-controller/src/PerpsController.ts index e6391f735c..9e3c03712d 100644 --- a/packages/perps-controller/src/PerpsController.ts +++ b/packages/perps-controller/src/PerpsController.ts @@ -1236,6 +1236,9 @@ export class PerpsController extends BaseController< // Eagerly hydrate in-memory caches from disk so hooks see data on first render. // Must happen at construction time — before any React component mounts. this.#hydrateCacheFromDiskSync(); + this.#options.infrastructure.performance.onControllerConstructed?.( + this.#options.infrastructure.performance.now(), + ); } // ============================================================================ @@ -4057,6 +4060,7 @@ export class PerpsController extends BaseController< PerpsMeasurementName.PerpsMarketDataPreload, performance.now() - preloadStart, 'millisecond', + traceId, ); } catch (error) { traceData = { @@ -4198,12 +4202,9 @@ export class PerpsController extends BaseController< provider: activeProvider, isTestnet, }, - data: { userAddress }, }); - this.#debugLog('PerpsController: Fetching user data in background', { - userAddress, - }); + this.#debugLog('PerpsController: Fetching user data in background'); if (activeProvider === 'hyperliquid') { const snapshot = await this.getUserDataSnapshot(); @@ -4221,6 +4222,7 @@ export class PerpsController extends BaseController< PerpsMeasurementName.PerpsUserDataPreload, performance.now() - preloadStart, 'millisecond', + traceId, ); return; } @@ -4335,6 +4337,7 @@ export class PerpsController extends BaseController< PerpsMeasurementName.PerpsUserDataPreload, performance.now() - preloadStart, 'millisecond', + traceId, ); } catch (error) { traceData = { diff --git a/packages/perps-controller/src/types/index.ts b/packages/perps-controller/src/types/index.ts index a49ee71eb4..a5e425843b 100644 --- a/packages/perps-controller/src/types/index.ts +++ b/packages/perps-controller/src/types/index.ts @@ -1937,8 +1937,20 @@ export type PerpsStreamManager = { */ export type PerpsPerformance = { now(): number; + /** + * Optional platform hook invoked once after constructor disk hydration. + * Receives `performance.now()` — not a Sentry write. + */ + onControllerConstructed?: (monotonicMs: number) => void; }; +type PerpsSetMeasurement = (( + name: string, + value: number, + unit: string, +) => void) & + ((name: string, value: number, unit: string, id: string) => void); + /** * Injectable tracer interface for Sentry/observability tracing. * Services use this to create spans and measure operation durations. @@ -1961,7 +1973,7 @@ export type PerpsTracer = { data?: Record; }): void; - setMeasurement(name: string, value: number, unit: string): void; + setMeasurement: PerpsSetMeasurement; addBreadcrumb(breadcrumb: { category: string; diff --git a/packages/perps-controller/tests/src/PerpsController.providers-cache.test.ts b/packages/perps-controller/tests/src/PerpsController.providers-cache.test.ts index 4e1d258757..ec40189984 100644 --- a/packages/perps-controller/tests/src/PerpsController.providers-cache.test.ts +++ b/packages/perps-controller/tests/src/PerpsController.providers-cache.test.ts @@ -1384,6 +1384,26 @@ describe('PerpsController', () => { ).toHaveLength(1); }); + it('publishes one construction timestamp after disk hydration and does not write Sentry at construct', () => { + const infra = createMockInfrastructure(); + (infra.performance.now as jest.Mock).mockReturnValue(321); + const onControllerConstructed = jest.fn(); + infra.performance.onControllerConstructed = onControllerConstructed; + + new TestablePerpsController({ + messenger: createMockMessenger(), + state: getDefaultPerpsControllerState(), + infrastructure: infra, + }); + + expect(onControllerConstructed).toHaveBeenCalledTimes(1); + expect(onControllerConstructed).toHaveBeenCalledWith(321); + expect( + (infra.diskCache.getItemSync as jest.Mock).mock.invocationCallOrder[0], + ).toBeLessThan(onControllerConstructed.mock.invocationCallOrder[0]); + expect(infra.tracer.setMeasurement).not.toHaveBeenCalled(); + }); + it('does not hydrate expired Terminal trend provenance from disk', () => { const infra = createMockInfrastructure(); (infra.diskCache.getItemSync as jest.Mock).mockImplementation( @@ -2115,6 +2135,13 @@ describe('PerpsController', () => { expect(mockInfrastructure.tracer.trace).toHaveBeenCalled(); expect(mockInfrastructure.tracer.endTrace).toHaveBeenCalled(); expect(mockInfrastructure.tracer.setMeasurement).toHaveBeenCalled(); + const traceId = mockInfrastructure.tracer.trace.mock.calls[0][0].id; + expect(mockInfrastructure.tracer.setMeasurement).toHaveBeenCalledWith( + expect.any(String), + expect.any(Number), + 'millisecond', + traceId, + ); }); }); @@ -2636,6 +2663,35 @@ describe('PerpsController', () => { ); }); + it('does not put userAddress on user-preload trace data and targets the named trace id', async () => { + preloadMockProvider.getMarketDataWithPrices.mockResolvedValue([]); + preloadMockProvider.getWebSocketConnectionState.mockReturnValue( + WSState.Disconnected, + ); + preloadController.testMarkInitialized(); + preloadController.testSetProviders( + new Map([['hyperliquid', preloadMockProvider]]), + ); + + preloadController.startMarketDataPreload(); + await jest.advanceTimersByTimeAsync(100); + + const userPreloadTrace = preloadInfrastructure.tracer.trace.mock.calls + .map((call) => call[0]) + .find((params) => params.name === 'Perps User Data Preload'); + expect(userPreloadTrace).toBeDefined(); + expect(userPreloadTrace?.data).toBeUndefined(); + expect(JSON.stringify(userPreloadTrace)).not.toContain( + mockEvmAccount.address, + ); + expect(preloadInfrastructure.tracer.setMeasurement).toHaveBeenCalledWith( + expect.any(String), + expect.any(Number), + 'millisecond', + userPreloadTrace?.id, + ); + }); + it.each([ [ 'provider',