-
-
Notifications
You must be signed in to change notification settings - Fork 360
Expand file tree
/
Copy pathmetroMiddleware.ts
More file actions
105 lines (92 loc) · 3.6 KB
/
metroMiddleware.ts
File metadata and controls
105 lines (92 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import type { StackFrame } from '@sentry/core';
import { addContextToFrame, logger } from '@sentry/core';
import { readFile } from 'fs';
import type { IncomingMessage, ServerResponse } from 'http';
import type { InputConfigT, Middleware } from 'metro-config';
import { promisify } from 'util';
import { SENTRY_CONTEXT_REQUEST_PATH, SENTRY_OPEN_URL_REQUEST_PATH } from '../metro/constants';
import { getRawBody } from '../metro/getRawBody';
import { openURLMiddleware } from '../metro/openUrlMiddleware';
const readFileAsync = promisify(readFile);
/**
* Accepts Sentry formatted stack frames and
* adds source context to the in app frames.
*/
export const stackFramesContextMiddleware: Middleware = async (
request: IncomingMessage,
response: ServerResponse,
): Promise<void> => {
logger.debug('[@sentry/react-native/metro] Received request for stack frames context.');
request.setEncoding('utf8');
const rawBody = await getRawBody(request);
let body: {
stack?: Partial<StackFrame>[];
} = {};
try {
body = JSON.parse(rawBody);
} catch (e) {
logger.debug('[@sentry/react-native/metro] Could not parse request body.', e);
badRequest(response, 'Invalid request body. Expected a JSON object.');
return;
}
const stack = body.stack;
if (!Array.isArray(stack)) {
logger.debug('[@sentry/react-native/metro] Invalid stack frames.', stack);
badRequest(response, 'Invalid stack frames. Expected an array.');
return;
}
const stackWithSourceContext = await Promise.all(stack.map(addSourceContext));
response.setHeader('Content-Type', 'application/json');
response.statusCode = 200;
response.end(JSON.stringify({ stack: stackWithSourceContext }));
logger.debug('[@sentry/react-native/metro] Sent stack frames context.');
};
async function addSourceContext(frame: StackFrame): Promise<StackFrame> {
if (!frame.in_app) {
return frame;
}
try {
if (typeof frame.filename !== 'string') {
logger.warn('[@sentry/react-native/metro] Could not read source context for frame without filename.');
return frame;
}
const source = await readFileAsync(frame.filename, { encoding: 'utf8' });
const lines = source.split('\n');
addContextToFrame(lines, frame);
} catch (error) {
logger.warn('[@sentry/react-native/metro] Could not read source context for frame.', error);
}
return frame;
}
function badRequest(response: ServerResponse, message: string): void {
response.statusCode = 400;
response.end(message);
}
/**
* Creates a middleware that adds source context to the Sentry formatted stack frames.
*/
export const createSentryMetroMiddleware = (middleware: Middleware): Middleware => {
return (request: IncomingMessage, response: ServerResponse, next: unknown) => {
if (request.url?.startsWith(`/${SENTRY_CONTEXT_REQUEST_PATH}`)) {
return stackFramesContextMiddleware(request, response);
} else if (request.url?.startsWith(`/${SENTRY_OPEN_URL_REQUEST_PATH}`)) {
return openURLMiddleware(request, response);
}
return middleware(request, response, next);
};
};
/**
* Adds the Sentry middleware to the Metro server config.
*/
export const withSentryMiddleware = (config: InputConfigT): InputConfigT => {
if (!config.server) {
// @ts-expect-error server is typed read only
config.server = {};
}
const originalEnhanceMiddleware = config.server.enhanceMiddleware;
config.server.enhanceMiddleware = (middleware, server) => {
const sentryMiddleware = createSentryMetroMiddleware(middleware);
return originalEnhanceMiddleware ? originalEnhanceMiddleware(sentryMiddleware, server) : sentryMiddleware;
};
return config;
};