@deeeed/metamask-harness 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/adapters/core/cleanup.sh +5 -29
  3. package/adapters/core/controller-import-probe.mjs +26 -0
  4. package/adapters/manifest.json +16 -0
  5. package/adapters/web-dapp/lib/policy-fence.mjs +119 -0
  6. package/adapters/web-dapp/lib/policy.mjs +7 -2
  7. package/dist/adapters/core/actions.js +0 -3
  8. package/dist/adapters/core/overlay.js +1 -5
  9. package/dist/adapters/core/readiness.js +133 -2
  10. package/dist/adapters/core/surface.js +53 -41
  11. package/dist/adapters/extension/extension-id.js +1 -0
  12. package/dist/adapters/extension/fixtures.js +60 -1
  13. package/dist/adapters/extension/network-observer.js +19 -206
  14. package/dist/adapters/extension/run.js +1 -1
  15. package/dist/adapters/extension/runtime.js +42 -3
  16. package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
  17. package/dist/adapters/mobile/performance-observer.js +9 -6
  18. package/dist/adapters/performance/trace-kinds.js +36 -0
  19. package/dist/adapters/plugin-surface.js +29 -1
  20. package/dist/adapters/web-dapp/run.js +6 -1
  21. package/dist/command-contract.js +34 -0
  22. package/dist/commands/check.js +3 -2
  23. package/dist/commands/core-readiness.js +1 -9
  24. package/dist/performance-observation.js +28 -5
  25. package/library/actions/core/perps/_controller.mjs +10 -0
  26. package/library/actions/extension/wallet/import.mjs +21 -4
  27. package/library/actions/extension/wallet/visible-session.mjs +32 -12
  28. package/package.json +7 -6
  29. package/dist/adapters/core/live-script-env.js +0 -36
  30. package/dist/adapters/extension/browser-cdp.js +0 -174
  31. package/dist/adapters/extension/performance-observer.js +0 -75
  32. package/dist/adapters/performance/cdp-trace.js +0 -406
  33. package/dist/adapters/performance/js-task-metrics.js +0 -35
  34. package/dist/async.js +0 -19
@@ -47,13 +47,6 @@ function coreDependencyBlock(target) {
47
47
  }
48
48
  return null;
49
49
  }
50
- function corePnpNodeOptions(target, current) {
51
- if (readYarnNodeLinker(target) !== "pnp") return current;
52
- const pnp = path.join(path.resolve(target), ".pnp.cjs");
53
- if (!fs.existsSync(pnp)) return current;
54
- const pnpOption = `--require ${pnp}`;
55
- return current ? `${current} ${pnpOption}` : pnpOption;
56
- }
57
50
  function canResolveFromTarget(specifier, target) {
58
51
  try {
59
52
  requireFromHarness.resolve(specifier, { paths: [target] });
@@ -75,6 +68,5 @@ function shellQuote(value) {
75
68
  return `'${value.replace(/'/gu, `'\\''`)}'`;
76
69
  }
77
70
  export {
78
- coreDependencyBlock,
79
- corePnpNodeOptions
71
+ coreDependencyBlock
80
72
  };
@@ -1,11 +1,17 @@
1
+ import { createRequire } from "node:module";
1
2
  import path from "node:path";
2
3
  import { promisify } from "node:util";
3
4
  import { gzip } from "node:zlib";
4
5
  import { getAdapterSurface } from "./adapters/surface.js";
5
- import { createExtensionPerformanceBackend } from "./adapters/extension/performance-observer.js";
6
- import { TRACE_KINDS } from "./adapters/performance/cdp-trace.js";
7
- import { summarizeFrames } from "@farmslot/adapter-rn";
8
- import { summarizeJavaScriptTasks } from "./adapters/performance/js-task-metrics.js";
6
+ import { resolveExtensionId } from "./adapters/extension/extension-id.js";
7
+ import {
8
+ TRACE_KINDS,
9
+ TRACE_MARKER_PREFIX
10
+ } from "./adapters/performance/trace-kinds.js";
11
+ import {
12
+ summarizeFrames,
13
+ summarizeJavaScriptTasks
14
+ } from "@farmslot/recipe-runner/runtime/cdp-trace";
9
15
  import {
10
16
  indexArtifactManifest,
11
17
  readContainedJsonArtifact,
@@ -512,7 +518,24 @@ function unavailableCapture(session, window, reason) {
512
518
  );
513
519
  }
514
520
  async function createExtensionPerformanceObserver(target, env) {
515
- return createExtensionPerformanceBackend(extensionCdpPort(env), target);
521
+ const cdpPort = extensionCdpPort(env);
522
+ const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
523
+ if (!extensionId) {
524
+ throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
525
+ }
526
+ const { createExtensionPerformanceBackend } = createRequire(import.meta.url)(
527
+ "@farmslot/adapter-web/performance-observer"
528
+ );
529
+ return createExtensionPerformanceBackend({
530
+ cdpPort,
531
+ extensionId,
532
+ uiPaths: ["/home.html", "/sidepanel.html"],
533
+ kind: TRACE_KINDS.extension,
534
+ platform: "extension",
535
+ markerPrefix: TRACE_MARKER_PREFIX,
536
+ connectTimeoutMs: 1e4,
537
+ commandTimeoutMs: 1e4
538
+ });
516
539
  }
517
540
  function extensionCdpPort(env) {
518
541
  const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
@@ -904,6 +904,16 @@ export async function runAdapter(callback) {
904
904
  // Surface the failure; still tear down so a half-open WebSocket from a
905
905
  // failed write doesn't leave the process wedged open.
906
906
  process.stderr.write(`[core/perps] adapter failed: ${fmtError(error)}\n`);
907
+ // A module the checkout cannot resolve is its environment (a partial
908
+ // workspace build), not app logic: say so on its own line, with the
909
+ // build step, for the runner's environment classification to match.
910
+ if (error?.code === 'ERR_MODULE_NOT_FOUND' || error?.code === 'MODULE_NOT_FOUND') {
911
+ const root = input.context?.projectRoot ?? process.cwd();
912
+ const missing = error.url ?? String(error.message ?? '').split('\n')[0];
913
+ process.stderr.write(
914
+ `[core/perps] environment: cannot resolve ${missing}\n Next: cd '${root.replace(/'/gu, `'\\''`)}' && yarn build\n`,
915
+ );
916
+ }
907
917
  }
908
918
  } finally {
909
919
  await disconnectController(exitCode);
@@ -100,6 +100,23 @@ async function unlockProfile(page, credentials, timeoutMs) {
100
100
  await waitForSelectorToDisappear(page, SELECTORS.unlockPassword, timeoutMs);
101
101
  }
102
102
 
103
+ // A locked profile is unlocked before the account proof. A fresh profile can
104
+ // also lock right after onboarding while the sidepanel attaches, stranding the
105
+ // proof on #/unlock: that relock is recovered once with the fixture password and
106
+ // reported as relockRecovered; a further lock fails with the route error.
107
+ async function ensureUnlockedSelectedAccount(page, credentials, timeoutMs) {
108
+ if (await hasVisibleSelector(page, SELECTORS.unlockPassword)) await unlockProfile(page, credentials, timeoutMs);
109
+ try {
110
+ const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
111
+ return { ...visibleSession, relockRecovered: false };
112
+ } catch (error) {
113
+ if (!(await hasVisibleSelector(page, SELECTORS.unlockPassword))) throw error;
114
+ await unlockProfile(page, credentials, timeoutMs);
115
+ const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
116
+ return { ...visibleSession, relockRecovered: true };
117
+ }
118
+ }
119
+
103
120
  async function importThroughUi(page, input, timeoutMs) {
104
121
  const credentials = await resolveWalletImportCredentials(input);
105
122
  const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
@@ -149,7 +166,7 @@ async function importThroughUi(page, input, timeoutMs) {
149
166
  await waitForSelector(page, SELECTORS.complete, timeoutMs);
150
167
  await page.click(SELECTORS.complete);
151
168
 
152
- const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
169
+ const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
153
170
  return { credentials, visibleSession, metametricsEnabled };
154
171
  }
155
172
 
@@ -168,9 +185,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
168
185
  if (!onboarding) {
169
186
  const credentials = await resolveWalletImportCredentials(input);
170
187
  const completionTriggered = await dismissVisibleOnboardingCompletion(page);
171
- const locked = await hasVisibleSelector(page, SELECTORS.unlockPassword);
172
- if (locked) await unlockProfile(page, credentials, timeoutMs);
173
- const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
188
+ const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
174
189
  return {
175
190
  action: input.action,
176
191
  method: 'ui-visible-session',
@@ -191,6 +206,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
191
206
  selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
192
207
  selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
193
208
  selectionPerformed: imported.visibleSession.selectionPerformed,
209
+ relockRecovered: imported.visibleSession.relockRecovered,
194
210
  completedOnboarding: true,
195
211
  metametricsEnabled: imported.metametricsEnabled,
196
212
  interests: [],
@@ -213,6 +229,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
213
229
  selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
214
230
  selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
215
231
  selectionPerformed: imported.visibleSession.selectionPerformed,
232
+ relockRecovered: imported.visibleSession.relockRecovered,
216
233
  completedOnboarding: true,
217
234
  metametricsEnabled: imported.metametricsEnabled,
218
235
  interests: [],
@@ -44,7 +44,7 @@ async function currentRoute(page, deadline) {
44
44
  return route.replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200);
45
45
  }
46
46
 
47
- async function routeError(page, message, deadline) {
47
+ async function routeError(page, message, deadline, cause) {
48
48
  let route = '<deadline-expired>';
49
49
  if (Date.now() < deadline) {
50
50
  try {
@@ -53,7 +53,35 @@ async function routeError(page, message, deadline) {
53
53
  route = '<unavailable>';
54
54
  }
55
55
  }
56
- return new Error(`${message} (route ${JSON.stringify(route)}).`);
56
+ const detail = cause
57
+ ? `; last input error: ${String(cause.message ?? cause).replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200)}`
58
+ : '';
59
+ return new Error(`${message} (route ${JSON.stringify(route)})${detail}.`);
60
+ }
61
+
62
+ // Entry can fail transiently while the account list settles after onboarding,
63
+ // so it is retried in place until the shared deadline; leaving the list (a
64
+ // lock, a redirect) fails at once with the last input error. Callers still
65
+ // assert the rendered result, so a retry can never pass a wrong account.
66
+ async function enterAccountSearch(page, value, deadline, message) {
67
+ let lastError;
68
+ while (Date.now() <= deadline) {
69
+ try {
70
+ await withinDeadline(page.setInput(ACCOUNT_SEARCH, value), deadline);
71
+ return;
72
+ } catch (error) {
73
+ lastError = error;
74
+ }
75
+ let route;
76
+ try {
77
+ route = await currentRoute(page, deadline);
78
+ } catch {
79
+ break;
80
+ }
81
+ if (route !== '#/account-list') break;
82
+ await new Promise((resolve) => setTimeout(resolve, 100));
83
+ }
84
+ throw await routeError(page, message, deadline, lastError);
57
85
  }
58
86
 
59
87
  async function visibleAccountMatch(page, deadline) {
@@ -140,11 +168,7 @@ async function openAccountList(page, deadline) {
140
168
 
141
169
  async function proveAddressSearchControl(page, expectedAddress, deadline) {
142
170
  const probeAddress = accountProbeAddress(expectedAddress);
143
- try {
144
- await withinDeadline(page.setInput(ACCOUNT_SEARCH, probeAddress), deadline);
145
- } catch {
146
- throw await routeError(page, 'Visible wallet account search could not enter the alternate-address control', deadline);
147
- }
171
+ await enterAccountSearch(page, probeAddress, deadline, 'Visible wallet account search could not enter the alternate-address control');
148
172
  let consecutiveEmpty = 0;
149
173
  while (Date.now() <= deadline) {
150
174
  let observed;
@@ -166,11 +190,7 @@ async function proveAddressSearchControl(page, expectedAddress, deadline) {
166
190
 
167
191
  async function queryExpectedAccount(page, expectedAddress, deadline, requireSelected) {
168
192
  await proveAddressSearchControl(page, expectedAddress, deadline);
169
- try {
170
- await withinDeadline(page.setInput(ACCOUNT_SEARCH, expectedAddress), deadline);
171
- } catch {
172
- throw await routeError(page, 'Visible wallet account search could not enter the derived release identity', deadline);
173
- }
193
+ await enterAccountSearch(page, expectedAddress, deadline, 'Visible wallet account search could not enter the derived release identity');
174
194
  let observed;
175
195
  while (Date.now() <= deadline) {
176
196
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,14 +21,15 @@
21
21
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
22
22
  },
23
23
  "dependencies": {
24
- "@farmslot/adapter-rn": "0.17.1",
25
- "@farmslot/adapter-sdk": "0.5.0",
26
- "@farmslot/adapter-web": "0.3.1",
24
+ "@farmslot/adapter-node": "0.1.2",
25
+ "@farmslot/adapter-rn": "0.18.0",
26
+ "@farmslot/adapter-sdk": "0.6.0",
27
+ "@farmslot/adapter-web": "0.4.1",
27
28
  "@farmslot/agent-runtime": "0.17.0",
28
29
  "@farmslot/handoff": "^0.3.1",
29
30
  "@farmslot/protocol": "0.34.0",
30
- "@farmslot/recipe-cli": "0.6.0",
31
- "@farmslot/recipe-runner": "0.25.0",
31
+ "@farmslot/recipe-cli": "0.8.0",
32
+ "@farmslot/recipe-runner": "0.26.0",
32
33
  "agent-device": "0.19.3",
33
34
  "commander": "^12.0.0",
34
35
  "esbuild": "0.28.1",
@@ -1,36 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { corePnpNodeOptions } from "../../commands/core-readiness.js";
5
- const CORE_WORKSPACE_PACKAGES = [
6
- "@metamask/base-controller",
7
- "@metamask/messenger",
8
- "@metamask/controller-utils",
9
- "@metamask/keyring-controller"
10
- ];
11
- async function coreLiveScriptEnv(projectRoot, tempDir) {
12
- const paths = {};
13
- for (const pkg of CORE_WORKSPACE_PACKAGES) {
14
- const packageDir = path.join(projectRoot, "packages", pkg.replace("@metamask/", ""));
15
- const distEntry = path.join(packageDir, "dist/index.cjs");
16
- if (existsSync(distEntry)) continue;
17
- const srcDir = path.join(packageDir, "src");
18
- if (!existsSync(path.join(srcDir, "index.ts"))) continue;
19
- paths[pkg] = [path.join(srcDir, "index.ts")];
20
- paths[`${pkg}/*`] = [path.join(srcDir, "*")];
21
- }
22
- if (Object.keys(paths).length === 0) return {};
23
- const tsconfigPath = path.join(tempDir, "core-adapter.tsconfig.json");
24
- await writeFile(
25
- tsconfigPath,
26
- `${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
27
- `
28
- );
29
- return {
30
- TSX_TSCONFIG_PATH: tsconfigPath,
31
- NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
32
- };
33
- }
34
- export {
35
- coreLiveScriptEnv
36
- };
@@ -1,174 +0,0 @@
1
- import { WebSocket } from "ws";
2
- import { withTimeout } from "../../async.js";
3
- const DEFAULT_TIMEOUT_MS = 1e4;
4
- async function createBrowserCdpClient(cdpPort) {
5
- if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
6
- throw new Error("Extension CDP requires a valid port.");
7
- }
8
- const version = asRecord(
9
- await fetchJson(`http://127.0.0.1:${cdpPort}/json/version`)
10
- );
11
- const webSocketUrl = String(version.webSocketDebuggerUrl ?? "");
12
- if (!webSocketUrl) {
13
- throw new Error("Extension browser CDP WebSocket is unavailable.");
14
- }
15
- const socket = new WebSocket(webSocketUrl);
16
- await waitForOpen(socket);
17
- let nextId = 0;
18
- const pending = /* @__PURE__ */ new Map();
19
- const handlers = /* @__PURE__ */ new Set();
20
- const closeHandlers = /* @__PURE__ */ new Set();
21
- socket.on("message", (data) => {
22
- let message;
23
- try {
24
- message = JSON.parse(String(data));
25
- } catch {
26
- return;
27
- }
28
- if (message.id && pending.has(message.id)) {
29
- const command = pending.get(message.id);
30
- pending.delete(message.id);
31
- if (!command) return;
32
- clearTimeout(command.timer);
33
- if (message.error) {
34
- command.reject(
35
- new Error(
36
- String(message.error.message ?? "Extension CDP command failed")
37
- )
38
- );
39
- } else {
40
- command.resolve(message.result ?? {});
41
- }
42
- return;
43
- }
44
- for (const handler of handlers) handler(message);
45
- });
46
- socket.on("close", () => {
47
- for (const command of pending.values()) {
48
- clearTimeout(command.timer);
49
- command.reject(new Error("Extension browser CDP connection closed"));
50
- }
51
- pending.clear();
52
- handlers.clear();
53
- for (const handler of closeHandlers) handler();
54
- closeHandlers.clear();
55
- });
56
- return {
57
- send(method, params = {}, sessionId, timeoutMs = DEFAULT_TIMEOUT_MS) {
58
- return new Promise((resolve, reject) => {
59
- const id = ++nextId;
60
- const timer = setTimeout(() => {
61
- pending.delete(id);
62
- reject(new Error(`Extension CDP command timed out: ${method}`));
63
- }, timeoutMs);
64
- pending.set(id, { resolve, reject, timer });
65
- socket.send(
66
- JSON.stringify({
67
- id,
68
- method,
69
- params,
70
- ...sessionId ? { sessionId } : {}
71
- })
72
- );
73
- });
74
- },
75
- onMessage(handler) {
76
- handlers.add(handler);
77
- return () => handlers.delete(handler);
78
- },
79
- onClose(handler) {
80
- closeHandlers.add(handler);
81
- return () => closeHandlers.delete(handler);
82
- },
83
- close() {
84
- socket.close();
85
- }
86
- };
87
- }
88
- async function fetchCdpJson(cdpPort, path = "/json") {
89
- return fetchJson(`http://127.0.0.1:${cdpPort}${path}`);
90
- }
91
- function asBrowserCdpTarget(value) {
92
- const target = asRecord(value);
93
- const targetId = String(target.targetId ?? target.id ?? "");
94
- const type = String(target.type ?? "");
95
- const url = String(target.url ?? "");
96
- return targetId && type && url ? { targetId, type, url } : null;
97
- }
98
- function extensionIdFromCdpTargets(value) {
99
- const targets = Array.isArray(value) ? value : [];
100
- for (const candidate of targets) {
101
- const target = asBrowserCdpTarget(candidate);
102
- if (!target) continue;
103
- try {
104
- const url = new URL(target.url);
105
- if (url.protocol === "chrome-extension:" && url.hostname) {
106
- return url.hostname;
107
- }
108
- } catch {
109
- continue;
110
- }
111
- }
112
- return null;
113
- }
114
- function selectExtensionUiTarget(value, extensionId) {
115
- const targets = Array.isArray(value) ? value : [];
116
- const candidates = targets.map(asBrowserCdpTarget).filter((candidate) => {
117
- if (!candidate || !["page", "other"].includes(candidate.type)) return false;
118
- try {
119
- const url = new URL(candidate.url);
120
- return url.protocol === "chrome-extension:" && url.hostname === extensionId;
121
- } catch {
122
- return false;
123
- }
124
- });
125
- const matchingPath = (target, pathname) => {
126
- try {
127
- return new URL(target.url).pathname === pathname;
128
- } catch {
129
- return false;
130
- }
131
- };
132
- const home = candidates.filter((target) => matchingPath(target, "/home.html"));
133
- if (home.length > 0) return home.length === 1 ? home[0] : null;
134
- const sidepanel = candidates.filter(
135
- (target) => matchingPath(target, "/sidepanel.html")
136
- );
137
- return sidepanel.length === 1 ? sidepanel[0] : null;
138
- }
139
- async function fetchJson(url) {
140
- const response = await fetch(url);
141
- if (!response.ok) {
142
- throw new Error(`Extension CDP returned HTTP ${response.status}.`);
143
- }
144
- return response.json();
145
- }
146
- function waitForOpen(socket) {
147
- const onOpen = (resolve) => () => resolve();
148
- let openHandler;
149
- let errorHandler;
150
- const opened = new Promise((resolve, reject) => {
151
- openHandler = onOpen(resolve);
152
- errorHandler = reject;
153
- socket.once("open", openHandler);
154
- socket.once("error", errorHandler);
155
- });
156
- return withTimeout(opened, {
157
- message: "Extension browser CDP connection timed out.",
158
- timeoutMs: DEFAULT_TIMEOUT_MS,
159
- onTimeout: () => socket.close()
160
- }).finally(() => {
161
- if (openHandler) socket.off("open", openHandler);
162
- if (errorHandler) socket.off("error", errorHandler);
163
- });
164
- }
165
- function asRecord(value) {
166
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
167
- }
168
- export {
169
- asBrowserCdpTarget,
170
- createBrowserCdpClient,
171
- extensionIdFromCdpTargets,
172
- fetchCdpJson,
173
- selectExtensionUiTarget
174
- };
@@ -1,75 +0,0 @@
1
- import {
2
- createCdpTraceCollector
3
- } from "../performance/cdp-trace.js";
4
- import {
5
- createBrowserCdpClient,
6
- fetchCdpJson,
7
- selectExtensionUiTarget
8
- } from "./browser-cdp.js";
9
- import { resolveExtensionId } from "./extension-id.js";
10
- async function createExtensionPerformanceBackend(cdpPort, target) {
11
- const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
12
- if (!extensionId) {
13
- throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
14
- }
15
- const uiTarget = selectExtensionUiTarget(
16
- await fetchCdpJson(cdpPort),
17
- extensionId
18
- );
19
- if (!uiTarget) {
20
- throw new Error("Extension performance observation requires an open MetaMask UI target.");
21
- }
22
- const browser = await createBrowserCdpClient(cdpPort);
23
- const attached = asRecord(
24
- await browser.send(
25
- "Target.attachToTarget",
26
- { targetId: uiTarget.targetId, flatten: true },
27
- void 0,
28
- 1e4
29
- )
30
- );
31
- const sessionId = String(attached.sessionId ?? "");
32
- if (!sessionId) {
33
- browser.close();
34
- throw new Error("Extension performance observation could not attach to the MetaMask UI target.");
35
- }
36
- const client = browserTraceClient(browser);
37
- return createCdpTraceCollector(client, "extension", async (name) => {
38
- const before = Date.now();
39
- await browser.send(
40
- "Runtime.evaluate",
41
- {
42
- expression: `performance.mark(${JSON.stringify(name)})`,
43
- returnByValue: true
44
- },
45
- sessionId,
46
- 1e4
47
- );
48
- const after = Date.now();
49
- return {
50
- hostEpochMs: (before + after) / 2,
51
- uncertaintyMs: (after - before) / 2
52
- };
53
- });
54
- }
55
- function browserTraceClient(browser) {
56
- return {
57
- send(method, params = {}, timeoutMs) {
58
- return browser.send(method, params, void 0, timeoutMs);
59
- },
60
- on(method, handler) {
61
- return browser.onMessage((message) => {
62
- if (message.method === method) handler(message.params ?? {});
63
- });
64
- },
65
- close() {
66
- browser.close();
67
- }
68
- };
69
- }
70
- function asRecord(value) {
71
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
72
- }
73
- export {
74
- createExtensionPerformanceBackend
75
- };