Skip to content

Commit ba5474e

Browse files
krystofwoldrichantonis
authored andcommitted
feat(playground): Open Sentry in desktop browser
1 parent 7596131 commit ba5474e

File tree

6 files changed

+124
-46
lines changed

6 files changed

+124
-46
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const SENTRY_MIDDLEWARE_PATH = '__sentry';
2+
export const SENTRY_CONTEXT_REQUEST_PATH = `${SENTRY_MIDDLEWARE_PATH}/context`;
3+
export const SENTRY_OPEN_URL_REQUEST_PATH = `${SENTRY_MIDDLEWARE_PATH}/open-url`;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { IncomingMessage } from 'http';
2+
3+
/**
4+
* Get the raw body of a request.
5+
*/
6+
export function getRawBody(request: IncomingMessage): Promise<string> {
7+
return new Promise((resolve, reject) => {
8+
let data = '';
9+
request.on('data', chunk => {
10+
data += chunk;
11+
});
12+
request.on('end', () => {
13+
resolve(data);
14+
});
15+
request.on('error', reject);
16+
});
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { debug } from '@sentry/core';
2+
3+
import { getDevServer } from '../integrations/debugsymbolicatorutils';
4+
import { SENTRY_OPEN_URL_REQUEST_PATH } from './constants';
5+
6+
/**
7+
* Send request to the Metro Development Server Middleware to open a URL in the system browser.
8+
*/
9+
export function openURLInBrowser(url: string): void {
10+
// oxlint-disable-next-line typescript-eslint(no-floating-promises)
11+
fetch(`${getDevServer()?.url || '/'}${SENTRY_OPEN_URL_REQUEST_PATH}`, {
12+
method: 'POST',
13+
body: JSON.stringify({ url }),
14+
}).catch(e => {
15+
debug.error('Error opening URL:', e);
16+
});
17+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { IncomingMessage, ServerResponse } from 'http';
2+
3+
import { getRawBody } from './getRawBody';
4+
5+
/*
6+
* Prefix for Sentry Metro logs to make them stand out to the user.
7+
*/
8+
const S = '\u001b[45;1m SENTRY \u001b[0m';
9+
10+
let open: ((url: string) => Promise<void>) | undefined = undefined;
11+
12+
/**
13+
* Open a URL in the system browser.
14+
*
15+
* Inspired by https://github.com/react-native-community/cli/blob/a856ce027a6b25f9363a8689311cdd4416c0fc89/packages/cli-server-api/src/openURLMiddleware.ts#L17
16+
*/
17+
export async function openURLMiddleware(req: IncomingMessage, res: ServerResponse): Promise<void> {
18+
if (!open) {
19+
try {
20+
// oxlint-disable-next-line import/no-extraneous-dependencies
21+
open = require('open');
22+
} catch (e) {
23+
// noop
24+
}
25+
}
26+
27+
if (req.method === 'POST') {
28+
const body = await getRawBody(req);
29+
let url: string | undefined = undefined;
30+
31+
try {
32+
const parsedBody = JSON.parse(body) as { url?: string };
33+
url = parsedBody.url;
34+
} catch (e) {
35+
res.writeHead(400);
36+
res.end('Invalid request body. Expected a JSON object with a url key.');
37+
return;
38+
}
39+
40+
try {
41+
if (!url) {
42+
res.writeHead(400);
43+
res.end('Invalid request body. Expected a JSON object with a url key.');
44+
return;
45+
}
46+
47+
if (!open) {
48+
throw new Error('The "open" module is not available.');
49+
}
50+
51+
await open(url);
52+
} catch (e) {
53+
// oxlint-disable-next-line no-console
54+
console.log(`${S} Open: ${url}`);
55+
56+
res.writeHead(500);
57+
58+
if (!open) {
59+
res.end('Failed to open URL. The "open" module is not available.');
60+
} else {
61+
res.end('Failed to open URL.');
62+
}
63+
return;
64+
}
65+
66+
// eslint-disable-next-line no-console
67+
console.log(`${S} Opened URL: ${url}`);
68+
res.writeHead(200);
69+
res.end();
70+
}
71+
}

packages/core/src/js/playground/modal.tsx

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { debug } from '@sentry/core';
21
import * as React from 'react';
32
import { Animated, Image, Modal, Platform, Pressable, Text, useColorScheme, View } from 'react-native';
43

5-
import { getDevServer } from '../integrations/debugsymbolicatorutils';
6-
import { isExpo, isExpoGo, isWeb } from '../utils/environment';
4+
import { openURLInBrowser } from '../metro/openUrlInBrowser';
5+
import { isExpoGo, isWeb } from '../utils/environment';
76
import { bug as bugAnimation, hi as hiAnimation, thumbsup as thumbsupAnimation } from './animations';
87
import { nativeCrashExample, tryCatchExample, uncaughtErrorExample } from './examples';
98
import { bug as bugImage, hi as hiImage, thumbsup as thumbsupImage } from './images';
@@ -71,7 +70,6 @@ export const SentryPlayground = ({
7170
}
7271
};
7372

74-
const showOpenSentryButton = !isExpo();
7573
const isNativeCrashDisabled = isWeb() || isExpoGo() || __DEV__;
7674

7775
const animationContainerYPosition = React.useRef(new Animated.Value(0)).current;
@@ -158,15 +156,13 @@ export const SentryPlayground = ({
158156
justifyContent: 'space-evenly', // Space between buttons
159157
}}
160158
>
161-
{showOpenSentryButton && (
162-
<Button
163-
secondary
164-
title={'Open Sentry'}
165-
onPress={() => {
166-
openURLInBrowser(issuesStreamUrl);
167-
}}
168-
/>
169-
)}
159+
<Button
160+
secondary
161+
title={'Open Sentry'}
162+
onPress={() => {
163+
openURLInBrowser(issuesStreamUrl);
164+
}}
165+
/>
170166
<Button
171167
title={'Go to my App'}
172168
onPress={() => {
@@ -269,19 +265,3 @@ const Button = ({
269265
</View>
270266
);
271267
};
272-
273-
function openURLInBrowser(url: string): void {
274-
const devServer = getDevServer();
275-
if (devServer?.url) {
276-
// This doesn't work for Expo project with Web enabled
277-
// oxlint-disable-next-line typescript-eslint(no-floating-promises)
278-
fetch(`${devServer.url}open-url`, {
279-
method: 'POST',
280-
body: JSON.stringify({ url }),
281-
}).catch(e => {
282-
debug.error('Error opening URL:', e);
283-
});
284-
} else {
285-
debug.error('Dev server URL not available');
286-
}
287-
}

packages/core/src/js/tools/metroMiddleware.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { addContextToFrame, debug } from '@sentry/core';
66
import { readFile } from 'fs';
77
import { promisify } from 'util';
88

9+
import { SENTRY_CONTEXT_REQUEST_PATH, SENTRY_OPEN_URL_REQUEST_PATH } from '../metro/constants';
10+
import { getRawBody } from '../metro/getRawBody';
11+
import { openURLMiddleware } from '../metro/openUrlMiddleware';
12+
913
const readFileAsync = promisify(readFile);
1014

1115
/**
@@ -71,29 +75,15 @@ function badRequest(response: ServerResponse, message: string): void {
7175
response.end(message);
7276
}
7377

74-
function getRawBody(request: IncomingMessage): Promise<string> {
75-
return new Promise((resolve, reject) => {
76-
let data = '';
77-
request.on('data', chunk => {
78-
data += chunk;
79-
});
80-
request.on('end', () => {
81-
resolve(data);
82-
});
83-
request.on('error', reject);
84-
});
85-
}
86-
87-
const SENTRY_MIDDLEWARE_PATH = '/__sentry';
88-
const SENTRY_CONTEXT_REQUEST_PATH = `${SENTRY_MIDDLEWARE_PATH}/context`;
89-
9078
/**
9179
* Creates a middleware that adds source context to the Sentry formatted stack frames.
9280
*/
9381
export const createSentryMetroMiddleware = (middleware: Middleware): Middleware => {
9482
return (request: IncomingMessage, response: ServerResponse, next: () => void) => {
95-
if (request.url?.startsWith(SENTRY_CONTEXT_REQUEST_PATH)) {
83+
if (request.url?.startsWith(`/${SENTRY_CONTEXT_REQUEST_PATH}`)) {
9684
return stackFramesContextMiddleware(request, response, next);
85+
} else if (request.url?.startsWith(`/${SENTRY_OPEN_URL_REQUEST_PATH}`)) {
86+
return openURLMiddleware(request, response);
9787
}
9888
return (middleware as (req: IncomingMessage, res: ServerResponse, next: () => void) => void)(
9989
request,

0 commit comments

Comments
 (0)