Skip to content

feat(playground): Open Sentry in desktop browser in Expo #4918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7bc6427
feat(wizard): Add Wizard component and integrate into Expo sample app
krystofwoldrich May 8, 2025
b670dd2
add mock data, fix visuals
krystofwoldrich May 9, 2025
9e74dbd
Add light style and open in browser button
krystofwoldrich May 10, 2025
ebcf43e
add better error examples, animate sentaur
krystofwoldrich May 13, 2025
e9459b5
use playground naming, add hoc for easy access
krystofwoldrich May 13, 2025
b9e6836
fix lint
krystofwoldrich May 18, 2025
6f00c5a
feat(playground): Open Sentry in desktop browser
krystofwoldrich May 18, 2025
3d4bf06
Merge remote-tracking branch 'origin/main' into kw-starter-wizard
krystofwoldrich May 28, 2025
63068a8
Merge branch 'kw-starter-wizard' into kw-starter-wizard-open-url
krystofwoldrich May 28, 2025
11bb145
Merge branch 'main' into kw-starter-wizard
krystofwoldrich Jun 12, 2025
d10b428
update assets to avoid inclusion when playground is not in use
krystofwoldrich Jun 12, 2025
6a8f264
move examples to a standalone file
krystofwoldrich Jun 12, 2025
1768320
Add sentry/react-native/playground export and tests
krystofwoldrich Jun 12, 2025
1510545
fix lint and snaps
krystofwoldrich Jun 13, 2025
a512bb0
add changelog
krystofwoldrich Jun 13, 2025
72718ed
Merge remote-tracking branch 'origin/main' into kw-starter-wizard
krystofwoldrich Jun 13, 2025
994844c
fix message
krystofwoldrich Jun 13, 2025
8f2c4e1
Merge branch 'kw-starter-wizard' into kw-starter-wizard-open-url
krystofwoldrich Jun 13, 2025
ca371c6
fix changelog
krystofwoldrich Jun 13, 2025
1636b83
Merge branch 'main' into kw-starter-wizard-open-url
lucas-zimerman Jun 16, 2025
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