Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

In Support of Remix #4946

dcramer started this conversation in General
Apr 15, 2022 · 7 comments · 13 replies
Discussion options

I've been working on a small app in Remix, and as part of that I wanted to take the opportunity to explore what it'd take for Sentry to support Remix.

Remix is intended to be more of a blend vs a full on "do it exactly this way" framework. What that means is we may have a more difficult time supporting it in our typical low friction methods. There currently are not straight forward injection points for a variety of things we'd typically look for. I'll try to break down my findings and update this as needed.

First, the frontend feels somewhat doable, but the backend feels like it's going to be a lot more effort for end-users. Below I'm articulating some concerns I've found with approaching our normal routes to injecting an SDK.

We're required to inject SENTRY_DSN ourselves somehow

That comes from the fact that we cannot access process.env in the frontend build by default. I've talked w/ the Remix team and nudged them towards the needs here. AFAICT they took an approach that "secrets are dangerous", and so their first pass approach was to require use of them via server-side hydration only.

The simplest approach to this today involves the user augmenting their root.tsx:

export const loader: LoaderFunction = async () => {
 return json<LoaderData>(
 {
 ENV: {
 SENTRY_DSN: process.env.SENTRY_DSN,
 },
 },
 );
};
export default function App() {
 const { ENV } = useLoaderData();
 return (
 // ...
 <script
 dangerouslySetInnerHTML={{
 __html: `window.ENV = ${JSON.stringify(ENV)}`,
 }}
 />
 );
}

A standard entrypoint happens after application logic could have defects

What this is doing is simply re-binding SENTRY_DSN as a global available via window. I then took this and initialized the SDK in entry.client.tsx. There are some issues with this approach as its already after application logic has begun to run, meaning we may not be able to capture all errors (which is not up to our standards):

import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";
import * as Sentry from "@sentry/react";
Sentry.init({
 tracesSampleRate: 1.0,
 dsn: window?.ENV?.SENTRY_DSN,
});
hydrate(<RemixBrowser />,document);

Server entry points sound like they need to be adapter specific

I'm still working through how we can implement server-side support, but it appears in the example app (the "Blues" stack) it is not reasonably doable. It's been suggested to me by the Remix team that people will generally stop using the built-in express server, and instead create their own, in which case we could register a Sentry middleware. I dont love this as it violates our low friction policy in Sentry. It's less of a problem if this was the only instrumentation they had to register, but when you combine it with the rest it starts to add up to a lot. I'm pushing for ways for us to avoid something similar to Next.js here which is very atypical for us given the amount of config we have to generate for the user.

What I have tried here, and has not achieved what was expected, was injecting into entry.server.tsx. As best I can tell this this is similar to entry.client.tsx, but actually runs earlier in the lifecycle (more ideal). Specifically my understanding is this runs when React needs to hydrate the initial document. That's potentially great for letting us capture hydration errors, but it falls short of achieving full server-side instrumentation.

Unclear solution to instrument loaders and actions

One big concern right now - and one I haven't yet been able to explore - is how we'd approach instrumenting the loaders (basically "fetch data" operations) and actions (basically "save data" operations). These are critical to Remix application development, and while our Error hooks likely would cover these (assuming we inject them early enough), I've yet to find a clean way to wrap these calls without hijacking Remix internals. Without this we will be unable to develop any kind of reasonable performance instrumentation.

Transaction names within react-router

On the client-side I was able to add a hook which - when referenced within root.tsx - allows a reliable transaction name for the current route to be assigned. This isn't working in all cases, and specifically I believe it is not currently working at all on the server-side. I need to do more work here yet.

export function useSentry() {
 const matches = useMatches();
 useEffect(
 () => {
 Sentry.configureScope((scope) => {
 scope.setTransactionName(matches[matches.length - 1].id);
 });
 },
 matches.map((m) => m.id)
 );
}

Unclear solution to client-side transactions

It's likely we will need to fully rely on whatever can be achieved within react-router's event system (this may be a blocker right now?) to obtain client-side transactions.

I will update this as I continue to poke around at this, but otherwise this will hopefully be helpful information to continue discussions within the community.

You must be logged in to vote

Replies: 7 comments 13 replies

Comment options

dcramer
Apr 15, 2022
Maintainer Author

Aside, I didn't even get to the point of discovery on webpack or versioning. I imagine it'll end up looking similar to Next.js in that regard.

You must be logged in to vote
0 replies
Comment options

dcramer
Apr 15, 2022
Maintainer Author

Lastly, relevant GH issue is here for SDK support: #4894

You must be logged in to vote
0 replies
Comment options

Hi @dcramer, thanks for taking a look at Remix. I'll do my best to help answer some of your questions here. I'll use the blues stack as a reference, though I'll also note that the purpose of the stacks is to show people one particular setup of Remix (running Express/node, hosted on fly.io, using Postgres as a db, etc.). There are actually many ways to use Remix with your fav host/framework/db/etc, but I'll leave that for another day. For now let's just get Sentry running in the blues stack.

Instrumenting the server

Let's start with the server. You can think of Remix like most typical React server-side rendering (SSR) setups. The server side goes like this:

  • boot a server (usually node)
  • request comes in, run the request handler (createRequestHandler in server.ts)
  • fetch some data
    • Remix does this by running the loaders in app/routes in parallel
  • render the page
  • send the response returned from the request handler
    • This is usually handled by your "server adapter", which in this case (the blues stack) is a standard Express middleware

Remix gives you direct access to both your server entry point and your React entry point for doing SSR. For purposes of instrumentation, you want the server entry point since this is the first code that runs when the server boots. From your guides on using Sentry with Node.js and with Express, just add the Sentry.init call to the top of your server.ts, use the Sentry Express middleware and you should be done.

Since you're running on node, you have full access to process.env, so just set your SENTRY_DSN as an environment variable in your server and use it directly:

import * as Sentry from "@sentry/node";
import "@sentry/tracing";
Sentry.init({
 dsn: process.env.SENTRY_DSN
 // ...
});
const app = express();
app.use(Sentry.Handlers.requestHandler());

Data loaders and actions

Your data loaders and actions also run server side in node, so capturing errors from them is the same as manually instrumenting any other node app.

// In one of your route modules...
export async function loader() {
 try {
 // talk to your db...
 } catch (error) {
 Sentry.captureException(error);
 }
}

Instrumenting the browser

The flow in the browser is a React hydrate workflow, which is standard when doing SSR in React. It goes like this:

As you noted above, there is a bit of extra work required to expose server environment variables to the client. Remix is unapologetically explicit here, not because we believe that "secrets are dangerous" but because we believe it's better to be explicit about exposing environment variables to front end code than to abstract it away in a config somewhere. In other words, we could create window.ENV for you, but then you'd probably wonder what it is and where it comes from. But if we allow you to create it yourself, you don't have to wonder about that.

Once you've done that (be sure to put your window.ENV <script> tag before <Scripts> in root.tsx), it's no different than your guide on usage with React. Just include your Sentry.init call immediately before you hydrate and you should be done.

import { hydrate } from "react-dom";
import * as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";
import { RemixBrowser } from "@remix-run/react";
Sentry.init({
 dsn: window.ENV.SENTRY_DSN,
 integrations: [new BrowserTracing()],
 tracesSampleRate: 1.0,
});
hydrate(<RemixBrowser />,document);

The Sentry docs use ReactDOM.render, and not hydrate, but I'm assuming Sentry works the same in either scenario.

Re:

There are some issues with this approach as its already after application logic has begun to run

That is not correct. The code in app/entry.client.tsx is the first code that runs in the browser. If you need something to run even earlier than our output bundles, you can always put your own <script> tag in app/root.tsx (like you did for setting up window.ENV, you have full control over the HTML of the page), but that shouldn't be required here. The only bundles that load before the app/entry.client.tsx bundle contain your dependencies that you need to run the page, like React and React Router. They shouldn't have any side effects.

And with that, you should be done! Again, this is all pretty typical for a React SSR setup, so I'm hopeful you can help me understand where our docs are unclear on this so we can try to avoid confusion in the future.

You must be logged in to vote
3 replies
Comment options

What about setUser – how/where do I call this in Remix to assign server errors to a user?

Can I just do it in loader of root.tsx?

Comment options

Looks like simply setting it in loader does not work. I tried setting setUser in root loader and then triggered error in one of the descending actions, and user is not associated with the error.

Comment options

@adaboese do you mind opening a new GH issue so we can help take a look? Thanks!

Comment options

dcramer
Apr 15, 2022
Maintainer Author

Quick update: I missed /server.ts in the blues app. Added in an express middleware so now I'm able to get some degree of instrumentation (e.g. prisma queries are not showing up in any of the transactions, and it's a bit fuzzy which transactions are express requests vs browser requests atm). I think directionally I can follow the flow though, and should be able to get something going.

You must be logged in to vote
0 replies
Comment options

We wanted to give you a little help getting this integration going. One of our engineers got started on a Remix + Sentry integration that supports good server stack traces, transactions, and individual loaders and actions. He setup a sample account to test things out.

You can fork the repo here: https://github.com/jacob-ebey/remix-sentry

Server stack traces:

Transactions:

Individual loader/action duration:

Web vitals:

The approach here is to take the compiled output of our server build and wrap things as needed. So e.g. the loaders and actions for an individual route can be wrapped so you can see how long they take and automatically capture errors.

This is just the beginning of a Remix + Sentry integration, and we don't plan on working much more on this code. So I'd suggest you fork it or just learn from it and take what you learn to build your own integration. Hopefully this should get you moving in the right direction!

You must be logged in to vote
0 replies
Comment options

dcramer
Apr 21, 2022
Maintainer Author

One last note, Remix is using esbuild, and so we'll need to somehow guide folks to update their build commands to include sourcemaps, and find a simplistic way to get them injecting sentry-cli into that pipline:

➜ ~/s/vanguard (main) ✔ esbuild --platform=node --format=cjs ./server.ts --outdir=build --sourcemap=external
 build/server.js 2.5kb
 build/server.js.map 3.9kb
You must be logged in to vote
0 replies
Comment options

We have made some progress on Sentry Remix SDK. And want to give an overview:

Client-side Instrumentation [Implemented]: #5264

  • We're using our React SDK to capture/report errors.
  • Implemented a router instrumentation specific to Remix projects, supporting parameterized route-based performance tracing.

Server-side Instrumentation [Implemented]: #5269

  • Using our Node.JS SDK to error tracking.
  • We're patching createRequestHandler from @remix-run/server-runtime, and wrap loader and action functions for performance tracing.
  • This also works with database integrations, such as Prisma.

Sourcemap Uploads [In Progress]

We can't implement automated sourcemap uploads, like we do with Next.JS SDK, as we could not find anything to hook our release/upload process. Mainly because the inner esbuild configuration / parameters are not exposed to the Remix users. (ref: remix-run/remix#2168 (comment))

But the default esbuild configuration of remix build --sourcemap outputs a usable set of linked sourcemaps, that we can upload with sentry-cli without a complex configuration.

So we're planning to ship a mini CLI tool with Remix SDK, which uses sentry-cli under the hood to create a release and upload sourcemaps with it.

Can look something like:

$ sentry-upload-sourcemaps <release> options[--urlPrefix, --buildPath]

That script will have defaults for --urlPrefix and --buildPath, so users won't need to define them unless they alter the build path on remix.config.js. sentry-cli can still be used, in case a more complex configuration is needed.

Any input would be greatly appreciated!

You must be logged in to vote
10 replies
Comment options

@kentcdodds it looks like they are parsing the remix server build and wrapping the actions and loaders https://github.com/getsentry/sentry-javascript/pull/5269/files#diff-754e32c1c14ac3c3c93eedeb7680548b986bc02a8b0bc63d2efc189210322acdR72-R130

So users shouldn't need to wrap there own loaders/actions that they export

Edit: at a glance I think it's loosely based on the example that jacob shared https://github.com/jacob-ebey/remix-sentry

Comment options

Awesome. That's what I thought. Just wanted to clarify. Thanks!

Comment options

Then I misunderstood.

All is fine as long as people don't need to wrap their action/loader themselves imo.

Comment options

Is this server-side piece designed to work on serverless platforms like Cloudflare? From my testing so far it seems like only the browser SDK is reporting anything. My actions/loaders don't produce any data in Sentry. I can't add environment variables to Sentry.init unless I'm inside handleRequest, so I've tried manually initializing at the top of entry.server.tsx like recommended in the README, just with a hardcoded DSN, as well as initializing inside handleRequest.

I'm getting awesome extensive reporting from the browser SDK but when I POST an action I don't see anything in Sentry.

Comment options

At the current moment the SDK doesn't support Cloudflare specifically as it isn't designed to run on Cloudflare worker runtime - but other serverless platforms (Netlify, Vercel), should work just fine.

@tulup-conner this is a great thing to bring up, I'm going to add it to our Roadmap! #4894

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /