Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/js/metro/constants.ts
Original file line number Diff line number Diff line change
@@ -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`;
17 changes: 17 additions & 0 deletions packages/core/src/js/metro/getRawBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { IncomingMessage } from 'http';

/**
* Get the raw body of a request.
*/
export function getRawBody(request: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let data = '';
request.on('data', chunk => {
data += chunk;
});
request.on('end', () => {
resolve(data);
});
request.on('error', reject);
});
}
17 changes: 17 additions & 0 deletions packages/core/src/js/metro/openUrlInBrowser.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
71 changes: 71 additions & 0 deletions packages/core/src/js/metro/openUrlMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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<void>) | 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<void> {
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();
}
}
29 changes: 8 additions & 21 deletions packages/core/src/js/playground/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,7 +83,6 @@ export const SentryPlayground = ({
}
};

const showOpenSentryButton = !isExpo();
const isNativeCrashDisabled = isWeb() || isExpoGo() || __DEV__;

const animationContainerYPosition = React.useRef(new Animated.Value(0)).current;
Expand Down Expand Up @@ -169,15 +169,13 @@ export const SentryPlayground = ({
justifyContent: 'space-evenly', // Space between buttons
}}
>
{showOpenSentryButton && (
<Button
secondary
title={'Open Sentry'}
onPress={() => {
openURLInBrowser(issuesStreamUrl);
}}
/>
)}
<Button
secondary
title={'Open Sentry'}
onPress={() => {
openURLInBrowser(issuesStreamUrl);
}}
/>
<Button
title={'Go to my App'}
onPress={() => {
Expand Down Expand Up @@ -421,14 +419,3 @@ const lightStyles: typeof defaultDarkStyles = StyleSheet.create({
backgroundColor: 'rgb(238, 235, 249)',
},
});

function openURLInBrowser(url: string): void {
// This doesn't work for Expo project with Web enabled
// disable-next-line @typescript-eslint/no-floating-promises
fetch(`${getDevServer().url}open-url`, {
method: 'POST',
body: JSON.stringify({ url }),
}).catch(e => {
logger.error('Error opening URL:', e);
});
}
24 changes: 7 additions & 17 deletions packages/core/src/js/tools/metroMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ 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);

/**
Expand Down Expand Up @@ -69,29 +73,15 @@ function badRequest(response: ServerResponse, message: string): void {
response.end(message);
}

function getRawBody(request: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let data = '';
request.on('data', chunk => {
data += chunk;
});
request.on('end', () => {
resolve(data);
});
request.on('error', reject);
});
}

const SENTRY_MIDDLEWARE_PATH = '/__sentry';
const SENTRY_CONTEXT_REQUEST_PATH = `${SENTRY_MIDDLEWARE_PATH}/context`;

/**
* 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)) {
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);
};
Expand Down
1 change: 1 addition & 0 deletions samples/expo/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function TabOneScreen() {
const { currentlyRunning } = useUpdates();
return (
<View style={styles.container}>
{/* <Image source={require('../..//hi.gif')} style={{ width: 100, height: 100 }} /> */}
<Sentry.TimeToInitialDisplay record />
<Text>Welcome to Sentry Expo Sample App!</Text>
<Text>Update ID: {currentlyRunning.updateId}</Text>
Expand Down
Loading