diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0d3c4aae..b2e4b71bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Features -- Introducing `@sentry/react-native/playground` ([#4916](https://github.com/getsentry/sentry-react-native/pull/4916)) +- Introducing `@sentry/react-native/playground` ([#4916](https://github.com/getsentry/sentry-react-native/pull/4916), [#4918](https://github.com/getsentry/sentry-react-native/pull/4918))) The new `withSentryPlayground` component allows developers to verify that the SDK is properly configured and reports errors as expected. diff --git a/packages/core/src/js/metro/constants.ts b/packages/core/src/js/metro/constants.ts new file mode 100644 index 0000000000..f8df251888 --- /dev/null +++ b/packages/core/src/js/metro/constants.ts @@ -0,0 +1,3 @@ +export const SENTRY_MIDDLEWARE_PATH = '__sentry'; +export const SENTRY_CONTEXT_REQUEST_PATH = `${SENTRY_MIDDLEWARE_PATH}/context`; +export const SENTRY_OPEN_URL_REQUEST_PATH = `${SENTRY_MIDDLEWARE_PATH}/open-url`; diff --git a/packages/core/src/js/metro/getRawBody.ts b/packages/core/src/js/metro/getRawBody.ts new file mode 100644 index 0000000000..69c5abc2c3 --- /dev/null +++ b/packages/core/src/js/metro/getRawBody.ts @@ -0,0 +1,17 @@ +import type { IncomingMessage } from 'http'; + +/** + * Get the raw body of a request. + */ +export function getRawBody(request: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let data = ''; + request.on('data', chunk => { + data += chunk; + }); + request.on('end', () => { + resolve(data); + }); + request.on('error', reject); + }); +} diff --git a/packages/core/src/js/metro/openUrlInBrowser.ts b/packages/core/src/js/metro/openUrlInBrowser.ts new file mode 100644 index 0000000000..133ce93dd3 --- /dev/null +++ b/packages/core/src/js/metro/openUrlInBrowser.ts @@ -0,0 +1,17 @@ +import { logger } from '@sentry/core'; + +import { getDevServer } from '../integrations/debugsymbolicatorutils'; +import { SENTRY_OPEN_URL_REQUEST_PATH } from './constants'; + +/** + * Send request to the Metro Development Server Middleware to open a URL in the system browser. + */ +export function openURLInBrowser(url: string): void { + // disable-next-line @typescript-eslint/no-floating-promises + fetch(`${getDevServer()?.url || '/'}${SENTRY_OPEN_URL_REQUEST_PATH}`, { + method: 'POST', + body: JSON.stringify({ url }), + }).catch(e => { + logger.error('Error opening URL:', e); + }); +} diff --git a/packages/core/src/js/metro/openUrlMiddleware.ts b/packages/core/src/js/metro/openUrlMiddleware.ts new file mode 100644 index 0000000000..c2d67a79d1 --- /dev/null +++ b/packages/core/src/js/metro/openUrlMiddleware.ts @@ -0,0 +1,71 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + +import { getRawBody } from './getRawBody'; + +/* + * Prefix for Sentry Metro logs to make them stand out to the user. + */ +const S = '\u001b[45;1m SENTRY \u001b[0m'; + +let open: ((url: string) => Promise) | undefined = undefined; + +/** + * Open a URL in the system browser. + * + * Inspired by https://github.com/react-native-community/cli/blob/a856ce027a6b25f9363a8689311cdd4416c0fc89/packages/cli-server-api/src/openURLMiddleware.ts#L17 + */ +export async function openURLMiddleware(req: IncomingMessage, res: ServerResponse): Promise { + if (!open) { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + open = require('open'); + } catch (e) { + // noop + } + } + + if (req.method === 'POST') { + const body = await getRawBody(req); + let url: string | undefined = undefined; + + try { + const parsedBody = JSON.parse(body) as { url?: string }; + url = parsedBody.url; + } catch (e) { + res.writeHead(400); + res.end('Invalid request body. Expected a JSON object with a url key.'); + return; + } + + try { + if (!url) { + res.writeHead(400); + res.end('Invalid request body. Expected a JSON object with a url key.'); + return; + } + + if (!open) { + throw new Error('The "open" module is not available.'); + } + + await open(url); + } catch (e) { + // eslint-disable-next-line no-console + console.log(`${S} Open: ${url}`); + + res.writeHead(500); + + if (!open) { + res.end('Failed to open URL. The "open" module is not available.'); + } else { + res.end('Failed to open URL.'); + } + return; + } + + // eslint-disable-next-line no-console + console.log(`${S} Opened URL: ${url}`); + res.writeHead(200); + res.end(); + } +} diff --git a/packages/core/src/js/playground/modal.tsx b/packages/core/src/js/playground/modal.tsx index 08d920aefe..1688ee84c0 100644 --- a/packages/core/src/js/playground/modal.tsx +++ b/packages/core/src/js/playground/modal.tsx @@ -14,6 +14,7 @@ import { View, } from 'react-native'; +import { openURLInBrowser } from '../metro/openUrlInBrowser'; import { getDevServer } from '../integrations/debugsymbolicatorutils'; import { isExpo, isExpoGo, isWeb } from '../utils/environment'; import { bug as bugAnimation, hi as hiAnimation, thumbsup as thumbsupAnimation } from './animations'; @@ -82,7 +83,6 @@ export const SentryPlayground = ({ } }; - const showOpenSentryButton = !isExpo(); const isNativeCrashDisabled = isWeb() || isExpoGo() || __DEV__; const animationContainerYPosition = React.useRef(new Animated.Value(0)).current; @@ -169,15 +169,13 @@ export const SentryPlayground = ({ justifyContent: 'space-evenly', // Space between buttons }} > - {showOpenSentryButton && ( -