Skip to content
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

feat(remix): Migrate to opentelemetry-instrumentation-remix. #12110

Merged
merged 27 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a173582
feat(remix): Migrate to `opentelemetry-instrumentation-remix`.
onurtemizkan May 17, 2024
f3b8ec8
Fix linter.
onurtemizkan May 17, 2024
ca48da0
Return jest config for unit tests back.
onurtemizkan May 17, 2024
07550da
Don't run Remix integration tests on Node 14
onurtemizkan May 17, 2024
f1e5c0c
Instrument function builds properly.
onurtemizkan May 17, 2024
ce315d3
Add ErrorBoundary propagation test.
onurtemizkan May 20, 2024
4ec9e3a
Export a no-op function in place of `wrapExpressCreateRequestHandler`.
onurtemizkan May 21, 2024
8419b7c
Add a custom `httpIntegration` for Remix.
onurtemizkan May 24, 2024
21d606d
Switch to `import` pattern to load instrumentation on Express tests
onurtemizkan May 29, 2024
4da9be8
Remove extra `Sentry.init`s in v2 e2e test app.
onurtemizkan May 29, 2024
0799c50
Make auto-instrumentation opt-in.
onurtemizkan Jun 4, 2024
bd3feeb
Add legacy integration tests back.
onurtemizkan Jun 5, 2024
d4678b1
Fix broken request transactions.
onurtemizkan Jun 7, 2024
d9de72a
Use Remix `httpIntegration` for both otel and legacy instrumentations.
onurtemizkan Jun 7, 2024
b39929d
Add Remix v1 legacy e2e tests.
onurtemizkan Jun 10, 2024
75977b7
Add Remix v2 legacy e2e tests.
onurtemizkan Jun 10, 2024
17eb45a
Test error boundary rendering on v2 integration tests.
onurtemizkan Jun 10, 2024
470749b
Add Remix/Express legacy e2e tests.
onurtemizkan Jun 10, 2024
dbb2b61
Fix passing client options.
onurtemizkan Jun 11, 2024
6c642a6
Fix typo
onurtemizkan Jun 11, 2024
673afef
Fix test init
onurtemizkan Jun 11, 2024
6145bda
Add missing `generateInstrumentOnce` exports.
onurtemizkan Jun 11, 2024
e6403fa
Update JSDocs.
onurtemizkan Jun 11, 2024
9fc99cd
Add missing `generateInstrumentOnce` export to @sentry/astro.
onurtemizkan Jun 11, 2024
5dd340f
Switch to `.mjs` in express tests.
onurtemizkan Jun 13, 2024
2cf2b91
Add test for loader span http.server connection.
onurtemizkan Jun 13, 2024
b44dd02
Move conditional logic inside `getRemixDefaultIntegrations`.
onurtemizkan Jun 13, 2024
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
7 changes: 4 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -912,10 +912,8 @@ jobs:
matrix:
node: [18, 20, 22]
remix: [1, 2]
# Remix v2 only supports Node 18+, so run Node 14, 16 tests separately
# Remix v2 only supports Node 18+, so run 16 tests separately
include:
- node: 14
mydea marked this conversation as resolved.
Show resolved Hide resolved
remix: 1
- node: 16
remix: 1
steps:
Expand Down Expand Up @@ -1037,8 +1035,11 @@ jobs:
'create-react-app',
'create-next-app',
'create-remix-app',
'create-remix-app-legacy',
'create-remix-app-v2',
'create-remix-app-v2-legacy',
'create-remix-app-express',
'create-remix-app-express-legacy',
'create-remix-app-express-vite-dev',
'node-express-esm-loader',
'node-express-esm-preload',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/

/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},

// Base config
extends: ['eslint:recommended'],

overrides: [
// React
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: ['react', 'jsx-a11y'],
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
],
settings: {
react: {
version: 'detect',
},
formComponents: ['Form'],
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
],
'import/resolver': {
typescript: {},
},
},
},

// Typescript
{
files: ['**/*.{ts,tsx}'],
plugins: ['@typescript-eslint', 'import'],
parser: '@typescript-eslint/parser',
settings: {
'import/internal-regex': '^~/',
'import/resolver': {
node: {
extensions: ['.ts', '.tsx'],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript'],
},

// Node
{
files: ['.eslintrc.cjs', 'server.js'],
env: {
node: true,
},
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { RemixBrowser, useLocation, useMatches } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import { StrictMode, startTransition, useEffect } from 'react';
import { hydrateRoot } from 'react-dom/client';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: window.ENV.SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration({
useEffect,
useLocation,
useMatches,
}),
Sentry.replayIntegration(),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
tunnel: 'http://localhost:3031/', // proxy server
});

Sentry.addEventProcessor(event => {
if (
event.type === 'transaction' &&
(event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation')
) {
const eventId = event.event_id;
if (eventId) {
window.recordedTransactions = window.recordedTransactions || [];
window.recordedTransactions.push(eventId);
}
}

return event;
});

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as Sentry from '@sentry/remix';

import { PassThrough } from 'node:stream';
import * as isbotModule from 'isbot';

import type { AppLoadContext, EntryContext } from '@remix-run/node';
import { createReadableStreamFromReadable } from '@remix-run/node';
import { installGlobals } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { renderToPipeableStream } from 'react-dom/server';

installGlobals();

const ABORT_DELAY = 5_000;

export const handleError = Sentry.wrapRemixHandleError;

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext,
) {
return isBotRequest(request.headers.get('user-agent'))
? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
: handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext);
}

// We have some Remix apps in the wild already running with isbot@3 so we need
// to maintain backwards compatibility even though we want new apps to use
// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev.
function isBotRequest(userAgent: string | null) {
if (!userAgent) {
return false;
}

// isbot >= 3.8.0, >4
if ('isbot' in isbotModule && typeof isbotModule.isbot === 'function') {
return isbotModule.isbot(userAgent);
}

// isbot < 3.8.0
if ('default' in isbotModule && typeof isbotModule.default === 'function') {
return isbotModule.default(userAgent);
}

return false;
}

function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);

setTimeout(abort, ABORT_DELAY);
});
}

function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);

setTimeout(abort, ABORT_DELAY);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { cssBundleHref } from '@remix-run/css-bundle';
import { LinksFunction, MetaFunction, json } from '@remix-run/node';
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
useRouteError,
} from '@remix-run/react';
import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix';
import type { SentryMetaArgs } from '@sentry/remix';

export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])];

export const loader = () => {
return json({
ENV: {
SENTRY_DSN: process.env.E2E_TEST_DSN,
},
});
};

export const meta = ({ data }: SentryMetaArgs<MetaFunction<typeof loader>>) => {
return [
{
env: data.ENV,
},
{
name: 'sentry-trace',
content: data.sentryTrace,
},
{
name: 'baggage',
content: data.sentryBaggage,
},
];
};

export function ErrorBoundary() {
const error = useRouteError();
const eventId = captureRemixErrorBoundaryError(error);

return (
<div>
<span>ErrorBoundary Error</span>
<span id="event-id">{eventId}</span>
</div>
);
}

function App() {
const { ENV } = useLoaderData();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(ENV)}`,
}}
/>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

export default withSentry(App);
Loading
Loading