Skip to content

Commit eea1afb

Browse files
committed
fix e2e sentry issue
1 parent 518a86c commit eea1afb

File tree

5 files changed

+207
-1
lines changed

5 files changed

+207
-1
lines changed

e2e/docs/CONTROLLER_MOCKING.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# E2E Controller Mocking
2+
3+
This document explains when and how to mock controller logic during E2E tests. This is not about Sentry; it focuses on replacing provider SDK interactions when needed, with the HyperLiquid provider as the concrete example.
4+
5+
## Context and Rationale
6+
7+
- Some features depend on external provider SDKs and live data streams (e.g., HyperLiquid market data via WebSocket). In E2E we need deterministic, reliable tests that are not affected by third-party uptime, timing, or data variability.
8+
- In these cases, we may replace controller interactions with the provider SDK so flows can proceed with stable, test-controlled data.
9+
- If your calls are plain HTTP/HTTPS and can be intercepted by our E2E mock server, prefer HTTP-level mocking instead (it’s simpler, more generic, and easier to maintain).
10+
11+
## Perps + HyperLiquid Controller Mixin (Specific Case)
12+
13+
- File: `e2e/controller-mocking/mock-config/perps-controller-mixin.ts`
14+
- Purpose: Replace HyperLiquid provider SDK touchpoints for E2E with safe, deterministic alternatives so the Perps connection lifecycle (initialization, subscriptions, reconnection) can be exercised without relying on the live SDK backend.
15+
16+
Key behaviors:
17+
18+
- Intercepts Perps controller methods that would call into the HyperLiquid SDK and redirects them to E2E-provided mocks.
19+
- Keeps the rest of the business logic intact while ensuring stable inputs/outputs for tests.
20+
21+
Scope note:
22+
23+
- This mixin is a solution for this particular provider and feature. Do not treat it as a general pattern for all network or SDK interactions.
24+
25+
## Prefer HTTP Mocking When Possible
26+
27+
- If your feature communicates over HTTP, use the E2E mock server (fixtures + proxy) to stub responses:
28+
- Faster to implement and review
29+
- Centralized and reusable across tests
30+
- Avoids coupling tests to controller internals
31+
32+
Only consider controller-level mocking when:
33+
34+
- The dependency is an SDK with complex transport (e.g., WebSockets) that isn’t easily intercepted by our mock server, or
35+
- You need to drive very specific edge cases not feasible at the network layer.
36+
37+
## How to Apply Controller Mocks
38+
39+
1. Implement a feature-scoped mixin/override under `e2e/controller-mocking/mock-config/`.
40+
2. Use your E2E bridge/initializer to inject the mixin into the running app for tests that require it.
41+
3. Keep overrides minimal and focused on stabilizing provider interactions. Avoid changing unrelated business logic.
42+
43+
## Best Practices
44+
45+
- Keep mocks deterministic and fast. Avoid timers and external dependencies.
46+
- Document each override and its purpose.
47+
- Align with the Page Object Model in specs; controller mocking should be configured via fixtures/bridge, not from the test body.
48+
- Prefer HTTP/network mocking where feasible; use controller mocking only when necessary.
49+
50+
## Notes
51+
52+
- This document is specifically about provider SDK replacement (e.g., HyperLiquid) for E2E stability. For third‑party library swaps at the module level, see `MODULE_MOCKING.md`.

e2e/docs/MODULE_MOCKING.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# E2E Module Mocking
2+
3+
This document explains why and how we mock native/SDK modules during E2E runs, with Sentry as the primary example.
4+
5+
## Context and Rationale
6+
7+
- In E2E, we run the app in bridgeless/debug-like conditions where some SDK integrations (e.g., Sentry tracing) are disabled or partially available.
8+
- Production code may assume fully functional tracing spans. When Sentry is disabled, closing or annotating spans can throw, breaking flows under test (e.g., Perps connection lifecycle).
9+
- To keep test reliability high and avoid feature-specific mixins, we alias third-party modules to E2E-friendly mocks at bundling time.
10+
11+
## What’s Mocked
12+
13+
- `@sentry/react-native`
14+
- `@sentry/core`
15+
16+
These are replaced with minimal, no-op implementations that preserve the public API shape used by our app code, including tracing functions.
17+
18+
## Where the Mocks Live
19+
20+
- `e2e/module-mocking/sentry/react-native.ts`
21+
- `e2e/module-mocking/sentry/core.ts`
22+
23+
Both files include safe no-ops and lightweight console logs for debugging (prefixed with `[E2E Sentry Mock]`).
24+
25+
## How Aliasing Works
26+
27+
Metro resolver is configured to alias Sentry packages to the E2E mocks when the E2E flag is set. The condition is:
28+
29+
- `IS_TEST === 'true'` or `METAMASK_ENVIRONMENT === 'e2e'`
30+
31+
This logic resides in `metro.config.js` via a custom `resolveRequest` that redirects requests for `@sentry/react-native` and `@sentry/core` to the mock files under `e2e/module-mocking/sentry/`.
32+
33+
## When to Use (Scope)
34+
35+
Use module-level aliasing sparingly. It is intended only for cross‑cutting, framework‑wide issues that affect many features or the entire E2E environment (e.g., global tracing integrations, core polyfills). If your need is feature‑specific, prefer scoped approaches instead (e.g., HTTP mocking via the mock server or controller‑level overrides).
36+
37+
## Extending Module Mocks
38+
39+
- Add new mock files under `e2e/module-mocking/<lib>/`.
40+
- Update `metro.config.js` `resolveRequest` to redirect the target module specifier to your mock file when E2E.
41+
- Keep APIs minimal; only implement members referenced by the app code to reduce maintenance.
42+
43+
## Notes
44+
45+
- This approach avoids per-feature mixins and centralizes E2E-specific behavior at the bundler level.
46+
- Production builds remain unaffected; aliasing only applies when E2E flags are set.

e2e/module-mocking/sentry/core.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Minimal Sentry Core mock for E2E runs
2+
3+
export interface Span {
4+
end?: (timestamp?: number) => void;
5+
setAttribute?: (key: string, value: unknown) => void;
6+
_name?: string;
7+
}
8+
9+
export interface StartSpanOptions {
10+
name?: string;
11+
op?: string;
12+
startTime?: number;
13+
parentSpan?: Span | null;
14+
attributes?: Record<string, unknown>;
15+
}
16+
17+
export const withIsolationScope = <T>(callback: (scope: unknown) => T): T => callback({ setTag: (_k: string, _v: unknown) => undefined });
18+
19+
export default {};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Minimal Sentry React Native mock for E2E runs
2+
3+
export interface Span {
4+
end?: (timestamp?: number) => void;
5+
setAttribute?: (key: string, value: unknown) => void;
6+
}
7+
8+
export const init = (_options?: unknown) => {
9+
// eslint-disable-next-line no-console
10+
console.log('[E2E Sentry Mock] init', _options ?? '(no options)');
11+
};
12+
13+
export const wrap = <T>(component: T): T => {
14+
// eslint-disable-next-line no-console
15+
console.log('[E2E Sentry Mock] wrap');
16+
return component;
17+
};
18+
19+
export const setMeasurement = (
20+
_name: string,
21+
_value: number,
22+
_unit?: string,
23+
) => {
24+
// eslint-disable-next-line no-console
25+
console.log('[E2E Sentry Mock] setMeasurement', _name, _value, _unit);
26+
};
27+
28+
export const startSpan = <T>(
29+
_options: unknown,
30+
callback: (span?: Span) => T,
31+
): T => {
32+
// eslint-disable-next-line no-console
33+
console.log('[E2E Sentry Mock] startSpan', _options);
34+
return callback(undefined);
35+
};
36+
37+
export const startSpanManual = <T>(
38+
_options: unknown,
39+
callback: (span?: Span) => T,
40+
): T => {
41+
// eslint-disable-next-line no-console
42+
console.log('[E2E Sentry Mock] startSpanManual', _options);
43+
return callback(undefined);
44+
};
45+
46+
// Optional helpers to reduce undefined checks in app code paths
47+
export const configureScope = (
48+
_fn: (scope: { setTag: (k: string, v: unknown) => void }) => void,
49+
) => {
50+
// eslint-disable-next-line no-console
51+
console.log('[E2E Sentry Mock] configureScope');
52+
};
53+
export const addBreadcrumb = (_breadcrumb: unknown) => {
54+
// eslint-disable-next-line no-console
55+
console.log('[E2E Sentry Mock] addBreadcrumb', _breadcrumb);
56+
};
57+
export const captureException = (_error: unknown) => {
58+
// eslint-disable-next-line no-console
59+
console.log('[E2E Sentry Mock] captureException', _error);
60+
};
61+
export const captureMessage = (_message: string) => {
62+
// eslint-disable-next-line no-console
63+
console.log('[E2E Sentry Mock] captureMessage', _message);
64+
};

metro.config.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ module.exports = function (baseConfig) {
4343
const {
4444
resolver: { assetExts, sourceExts },
4545
} = defaultConfig;
46+
const isE2E =
47+
process.env.IS_TEST === 'true' ||
48+
process.env.METAMASK_ENVIRONMENT === 'e2e';
4649

4750
// For less powerful machines, leave room to do other tasks. For instance,
4851
// if you have 10 cores but only 16GB, only 3 workers would get used.
@@ -56,7 +59,6 @@ module.exports = function (baseConfig) {
5659
resolver: {
5760
assetExts: [...assetExts.filter((ext) => ext !== 'svg'), 'riv'],
5861
sourceExts: [...sourceExts, 'svg', 'cjs', 'mjs'],
59-
resolverMainFields: ['sbmodern', 'react-native', 'browser', 'main'],
6062
extraNodeModules: {
6163
...defaultConfig.resolver.extraNodeModules,
6264
'node:crypto': require.resolve('react-native-crypto'),
@@ -80,6 +82,29 @@ module.exports = function (baseConfig) {
8082
buffer: '@craftzdog/react-native-buffer',
8183
'node:buffer': '@craftzdog/react-native-buffer',
8284
},
85+
resolveRequest: isE2E
86+
? (context, moduleName, platform) => {
87+
if (moduleName === '@sentry/react-native') {
88+
return {
89+
type: 'sourceFile',
90+
filePath: path.resolve(
91+
__dirname,
92+
'e2e/module-mocking/sentry/react-native.ts',
93+
),
94+
};
95+
}
96+
if (moduleName === '@sentry/core') {
97+
return {
98+
type: 'sourceFile',
99+
filePath: path.resolve(
100+
__dirname,
101+
'e2e/module-mocking/sentry/core.ts',
102+
),
103+
};
104+
}
105+
return context.resolveRequest(context, moduleName, platform);
106+
}
107+
: defaultConfig.resolver.resolveRequest,
83108
},
84109
transformer: {
85110
babelTransformerPath: require.resolve('./metro.transform.js'),

0 commit comments

Comments
 (0)