Skip to content

Commit c6acce5

Browse files
test: adds diagnosis for launcArhs monitoring
1 parent c99e886 commit c6acce5

4 files changed

Lines changed: 178 additions & 6 deletions

File tree

app/util/test/network-store.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import axios from 'axios';
22
import { Platform } from 'react-native';
3-
import { getFixturesServerPortInApp } from './utils';
3+
import {
4+
appendE2EDiagnosticsToUrl,
5+
getE2ETestConfigDiagnostics,
6+
getFixturesServerPortInApp,
7+
} from './utils';
48

59
const FETCH_TIMEOUT = 40000; // Timeout in milliseconds
610

@@ -90,15 +94,21 @@ class ReadOnlyNetworkStore {
9094
// 1. localhost with actual port (works on iOS, might work on Android with adb reverse)
9195
// 2. 10.0.2.2 with actual port (Android emulator host, direct access without adb reverse!)
9296
// 3. bs-local.com (BrowserStack)
93-
const urls = [`http://localhost:${port}/state.json`];
97+
const diagnostics = getE2ETestConfigDiagnostics({
98+
source: 'network-store',
99+
platform: Platform.OS,
100+
});
101+
const appendDiagnostics = (url) =>
102+
appendE2EDiagnosticsToUrl(url, diagnostics);
103+
const urls = [appendDiagnostics(`http://localhost:${port}/state.json`)];
94104

95105
// Android emulator can access host via 10.0.2.2 without adb reverse
96106
if (isAndroid) {
97-
urls.push(`http://10.0.2.2:${port}/state.json`);
107+
urls.push(appendDiagnostics(`http://10.0.2.2:${port}/state.json`));
98108
}
99109

100110
// BrowserStack uses bs-local.com
101-
urls.push(`http://bs-local.com:${port}/state.json`);
111+
urls.push(appendDiagnostics(`http://bs-local.com:${port}/state.json`));
102112

103113
try {
104114
for (const url of urls) {

app/util/test/utils.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
/* global globalThis */
2+
13
export const flushPromises = () => new Promise(setImmediate);
24

35
// Fallback ports - used in fixture data and when LaunchArgs are unavailable
46
// Android: These are mapped to actual PortManager-allocated ports via adb reverse
57
// iOS: These are overridden by LaunchArgs at runtime
68
export const FALLBACK_FIXTURE_SERVER_PORT = 12345;
79
export const FALLBACK_COMMAND_QUEUE_SERVER_PORT = 2446;
10+
export const E2E_TEST_CONFIG_GLOBAL_KEY = '__METAMASK_E2E_TEST_CONFIG__';
11+
export const E2E_DIAGNOSTICS_ENDPOINT = '/e2e-diagnostics';
812

9-
// E2E test configuration required in app
10-
export const testConfig = {};
13+
// E2E test configuration required in app. Metro/RN can evaluate this module
14+
// through more than one path during startup, so keep the backing object on
15+
// globalThis and make every module instance share the same runtime config.
16+
if (!globalThis[E2E_TEST_CONFIG_GLOBAL_KEY]) {
17+
globalThis[E2E_TEST_CONFIG_GLOBAL_KEY] = {};
18+
}
19+
export const testConfig = globalThis[E2E_TEST_CONFIG_GLOBAL_KEY];
1120

1221
// SEGMENT TRACK URL for E2E tests - this is not a real URL and is used for testing purposes only
1322
export const E2E_METAMETRICS_TRACK_URL = 'https://metametrics.test/track';
@@ -43,4 +52,42 @@ export const getFixturesServerPortInApp = () =>
4352
export const getCommandQueueServerPortInApp = () =>
4453
testConfig.commandQueueServerPort ?? FALLBACK_COMMAND_QUEUE_SERVER_PORT;
4554

55+
const formatE2EDiagnosticValue = (value) => {
56+
if (value === undefined) {
57+
return 'missing';
58+
}
59+
if (value === null) {
60+
return 'null';
61+
}
62+
return String(value);
63+
};
64+
65+
export const getE2ETestConfigDiagnostics = (extra = {}) => ({
66+
source: extra.source ?? 'unknown',
67+
phase: extra.phase ?? 'unknown',
68+
platform: extra.platform ?? 'unknown',
69+
fixtureServerPortInApp: getFixturesServerPortInApp(),
70+
commandQueueServerPortInApp: getCommandQueueServerPortInApp(),
71+
fixtureServerPortRaw: formatE2EDiagnosticValue(testConfig.fixtureServerPort),
72+
commandQueueServerPortRaw: formatE2EDiagnosticValue(
73+
testConfig.commandQueueServerPort,
74+
),
75+
mockServerPortRaw: formatE2EDiagnosticValue(testConfig.mockServerPort),
76+
hasGlobalE2EConfig:
77+
globalThis[E2E_TEST_CONFIG_GLOBAL_KEY] === testConfig ? 'true' : 'false',
78+
configKeys: Object.keys(testConfig).sort().join(',') || 'none',
79+
launchArgumentKeys: Array.isArray(testConfig.launchArgumentKeys)
80+
? testConfig.launchArgumentKeys.join(',')
81+
: 'missing',
82+
...extra,
83+
});
84+
85+
export const appendE2EDiagnosticsToUrl = (url, diagnostics) => {
86+
const nextUrl = new URL(url);
87+
Object.entries(diagnostics).forEach(([key, value]) => {
88+
nextUrl.searchParams.set(`e2e_${key}`, formatE2EDiagnosticValue(value));
89+
});
90+
return nextUrl.toString();
91+
};
92+
4693
export const isRc = process.env.METAMASK_ENVIRONMENT === 'rc';

shim.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import {
3333
} from 'react-native-quick-crypto';
3434
import { LaunchArguments } from 'react-native-launch-arguments';
3535
import {
36+
E2E_DIAGNOSTICS_ENDPOINT,
3637
FALLBACK_FIXTURE_SERVER_PORT,
3738
FALLBACK_COMMAND_QUEUE_SERVER_PORT,
39+
getE2ETestConfigDiagnostics,
3840
isE2E,
3941
isTest,
4042
enableApiCallLogs,
@@ -45,6 +47,16 @@ import { defaultMockPort } from './tests/api-mocking/mock-config/mockUrlCollecti
4547

4648
import './shimPerf';
4749

50+
const getLaunchArgumentKeys = (raw) =>
51+
raw && typeof raw === 'object' ? Object.keys(raw).sort() : [];
52+
53+
const recordLaunchArgumentDiagnostics = (raw) => {
54+
testConfig.launchArgumentKeys = getLaunchArgumentKeys(raw);
55+
testConfig.rawFixtureServerPort = raw?.fixtureServerPort ?? null;
56+
testConfig.rawCommandQueueServerPort = raw?.commandQueueServerPort ?? null;
57+
testConfig.rawMockServerPort = raw?.mockServerPort ?? null;
58+
};
59+
4860
// Needed to polyfill random number generation
4961
import 'react-native-get-random-values';
5062

@@ -108,12 +120,14 @@ if (isE2E) {
108120
// See FixtureHelper.ts for the port mapping implementation.
109121
if (isTest) {
110122
const raw = LaunchArguments.value();
123+
recordLaunchArgumentDiagnostics(raw);
111124
testConfig.fixtureServerPort = raw?.fixtureServerPort
112125
? raw.fixtureServerPort
113126
: FALLBACK_FIXTURE_SERVER_PORT;
114127
testConfig.commandQueueServerPort = raw?.commandQueueServerPort
115128
? raw.commandQueueServerPort
116129
: FALLBACK_COMMAND_QUEUE_SERVER_PORT;
130+
testConfig.mockServerPort = raw?.mockServerPort ?? defaultMockPort;
117131
}
118132

119133
// Fix for https://github.com/facebook/react-native/issues/5667
@@ -269,7 +283,9 @@ if (typeof localStorage !== 'undefined') {
269283
if (enableApiCallLogs || isTest) {
270284
(async () => {
271285
const raw = LaunchArguments.value();
286+
recordLaunchArgumentDiagnostics(raw);
272287
const mockServerPort = raw?.mockServerPort ?? defaultMockPort;
288+
testConfig.mockServerPort = mockServerPort;
273289
const { fetch: originalFetch } = global;
274290

275291
// eslint-disable-next-line no-console
@@ -315,6 +331,35 @@ if (enableApiCallLogs || isTest) {
315331
}
316332
}
317333

334+
const postE2EDiagnostics = async (phase) => {
335+
if (!isMockServerAvailable || !MOCKTTP_URL) {
336+
return;
337+
}
338+
339+
const diagnostics = getE2ETestConfigDiagnostics({
340+
source: 'shim',
341+
phase,
342+
platform: Platform.OS,
343+
mockServerUrl: MOCKTTP_URL,
344+
mockServerAvailable: String(isMockServerAvailable),
345+
launchArgFixtureServerPort: raw?.fixtureServerPort ?? 'missing',
346+
launchArgCommandQueueServerPort:
347+
raw?.commandQueueServerPort ?? 'missing',
348+
launchArgMockServerPort: raw?.mockServerPort ?? 'missing',
349+
});
350+
351+
await originalFetch(`${MOCKTTP_URL}${E2E_DIAGNOSTICS_ENDPOINT}`, {
352+
method: 'POST',
353+
headers: { 'Content-Type': 'application/json' },
354+
body: JSON.stringify(diagnostics),
355+
}).catch((error) => {
356+
// eslint-disable-next-line no-console
357+
console.warn(`[E2E SHIM] Failed to post diagnostics: ${error.message}`);
358+
});
359+
};
360+
361+
await postE2EDiagnostics('mock-server-connected');
362+
318363
// if mockServer is off we route to original destination
319364
global.fetch = async (url, options) => {
320365
// Extract URL string from Request or URL objects

tests/api-mocking/MockServerE2E.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,45 @@ interface LiveRequest {
9393
timestamp: string;
9494
}
9595

96+
interface E2EDiagnostic {
97+
timestamp: string;
98+
payload: unknown;
99+
}
100+
96101
export interface InternalMockServer extends Mockttp {
97102
_liveRequests?: LiveRequest[];
103+
_e2eDiagnostics?: E2EDiagnostic[];
98104
}
99105

106+
const MAX_E2E_DIAGNOSTICS = 20;
107+
const MAX_E2E_DIAGNOSTICS_IN_ERROR = 3;
108+
109+
const stringifyE2EDiagnostic = (payload: unknown): string => {
110+
try {
111+
return JSON.stringify(payload, null, 2);
112+
} catch {
113+
return String(payload);
114+
}
115+
};
116+
117+
const formatE2EDiagnostics = (diagnostics?: E2EDiagnostic[]): string => {
118+
if (!diagnostics || diagnostics.length === 0) {
119+
return 'E2E app diagnostics: none received.';
120+
}
121+
122+
const latestDiagnostics = diagnostics.slice(-MAX_E2E_DIAGNOSTICS_IN_ERROR);
123+
const diagnosticsSummary = latestDiagnostics
124+
.map(
125+
(diagnostic, index) =>
126+
`${index + 1}. ${diagnostic.timestamp}\n${stringifyE2EDiagnostic(
127+
diagnostic.payload,
128+
)}`,
129+
)
130+
.join('\n');
131+
132+
return `E2E app diagnostics (latest ${latestDiagnostics.length}/${diagnostics.length}):\n${diagnosticsSummary}`;
133+
};
134+
100135
/**
101136
* Translates fallback ports to actual allocated ports in URLs.
102137
* This allows the MockServer (running on host) to forward requests to dynamically allocated local resources.
@@ -278,6 +313,7 @@ export default class MockServerE2E implements Resource {
278313

279314
this._server = getLocal() as InternalMockServer;
280315
this._server._liveRequests = [];
316+
this._server._e2eDiagnostics = [];
281317
await this._server.start(this._serverPort);
282318

283319
logger.info(
@@ -287,6 +323,38 @@ export default class MockServerE2E implements Resource {
287323
await this._server
288324
.forGet('/health-check')
289325
.thenReply(200, 'Mock server is running');
326+
await this._server
327+
.forPost('/e2e-diagnostics')
328+
.thenCallback(async (request) => {
329+
const bodyText = await safeGetBodyText(request);
330+
let payload: unknown = bodyText;
331+
332+
if (bodyText) {
333+
try {
334+
payload = JSON.parse(bodyText);
335+
} catch {
336+
payload = bodyText;
337+
}
338+
}
339+
340+
const diagnostic = {
341+
timestamp: new Date().toISOString(),
342+
payload,
343+
};
344+
this._server?._e2eDiagnostics?.push(diagnostic);
345+
346+
if (
347+
this._server?._e2eDiagnostics &&
348+
this._server._e2eDiagnostics.length > MAX_E2E_DIAGNOSTICS
349+
) {
350+
this._server._e2eDiagnostics.shift();
351+
}
352+
353+
logger.debug(
354+
`Received E2E app diagnostics: ${stringifyE2EDiagnostic(payload)}`,
355+
);
356+
return { statusCode: 204, body: '' };
357+
});
290358
await this._server
291359
.forGet(
292360
/^http:\/\/(localhost|127\.0\.0\.1|10\.0\.2\.2)(:\\d+)?\/favicon\.ico$/,
@@ -854,8 +922,10 @@ export default class MockServerE2E implements Resource {
854922

855923
const totalCount = mockServer._liveRequests.length;
856924
const uniqueCount = uniqueRequests.length;
925+
const diagnosticsSummary = formatE2EDiagnostics(mockServer._e2eDiagnostics);
857926
const message =
858927
`Test made ${totalCount} unmocked request(s) (${uniqueCount} unique):\n${requestsSummary}\n\n` +
928+
`${diagnosticsSummary}\n\n` +
859929
"Check your test-specific mocks or add them to the default mocks.\n You can also add the URL to the allowlist if it's a known live request.";
860930
logger.error(message);
861931
throw new Error(message);

0 commit comments

Comments
 (0)