Skip to content

Instantly share code, notes, and snippets.

@dcramer
Last active June 14, 2023 16:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dcramer/2df1c3564ed6081bcea28ad144e3dff5 to your computer and use it in GitHub Desktop.
Save dcramer/2df1c3564ed6081bcea28ad144e3dff5 to your computer and use it in GitHub Desktop.
Running Remix Problems

Remix Issues

Transaction Names

useMatches only exposes the filepath, but we want a parameterized route:

8:53:24 AM web.1 |  {
8:53:24 AM web.1 |    id: 'routes/bottles.$bottleId._index',
8:53:24 AM web.1 |    pathname: '/bottles/4/',
8:53:24 AM web.1 |    params: { bottleId: '4' },
8:53:24 AM web.1 |    data: { dehydratedState: { mutations: [], queries: [Array] } },
8:53:24 AM web.1 |    handle: undefined
8:53:24 AM web.1 |  }

We want to effectively create the transaction description of /bottles/:$bottleId in this scenario. In Django when we first did this we simply implemented a regexp parser. We could do something similarly dumb simple here if Remix does not (and cannot) expose its route matcher.

TraceParent Injection

Note: This is critical to Remix working, and is glossed over in our onboarding.

There are two methods for injection in Remix via the meta tag (v1 and v2):

// V1
export function meta({ data }) {
  return {
    "trace-parent": data?.sentryTrace
    baggage: data?.sentryBaggage
  };
}
// V2
export default withSentry(function App() {
  return (
    <html lang="en" className="h-full">
      <head>
        <Meta />
        {data?.sentryTrace && (
          <meta name="sentry-trace" content={data.sentryTrace} />
        )}
        {data?.sentryBaggage && (
          <meta name="baggage" content={data.sentryBaggage} />
        )}
        <Links />
      </head>
    </html>
  )
});

Its possible V2 could be the mechanism for V1 as well, but realistically we'd prefer Remix provide an injection point that lets us insert into <Meta/>.

Note: You cannot use the V2 meta function to inject tracing headers, as meta gets overwritten by child routes in V2.

SDK Context (Server)

There are too many variations and caveats to SDK initialization. For example, in the default Remix scenario we suggest you use entry.server.ts, which happens after the runtime has initialized. While this might be ok, as the SDK gets initialized on import, there are other mechanisms where you might need to initialize the SDK earlier (and reference it).

For example, Remix's best method for managing sessions and other shared context within an Express-based runtime is via loader context (getLoaderContext). That happens outside of the scope, and is also an ideal place to bind shared context. While a middleware-based approach might solve for this down the road (ore a preLoader global hook), this is key for things like session information as loaders are executed in parallel (thus the root loader cant be a control point):

// An express middleware to bind various request state, wihch can then be referenced in `getLoaderContext`
app.all("*", async (req, res, next) => {
  const session = await getSession(req);
  const user = await getUser(session);
  const accessToken = await getAccessToken(session);

  Sentry.setUser({
    id: `${user?.id}`,
    username: user?.username,
    email: user?.email,
  });

  req.user = user || null;
  req.accessToken = accessToken || null;
  req.api = new ApiClient({
    server: config.API_SERVER,
    accessToken,
  });

  if (accessToken && !user) {
    return logout(req);
  }

  next();
});

We then bind that context:

app.all(
  "*",
  MODE === "production"
    ? createSentryRequestHandler({ build: require(BUILD_DIR), getLoadContext })
    : (...args) => {
        purgeRequireCache();
        const requestHandler = createSentryRequestHandler({
          build: require(BUILD_DIR),
          mode: MODE,
          getLoadContext,
        });
        return requestHandler(...args);
      },
);

Lastly, we would need to re-bind this context in our root component:

export async function loader({ context }) {
  return json({
    accessToken: context.accessToken,
    user: context.user,
  });
}

export default withSentry(function App() {
  const { accessToken, user } = useLoaderData<LoaderData>();

  Sentry.setUser(user ? {
    id: `${user?.id}`,
    username: user?.username,
    email: user?.email,
  } : null);
});

Server-to-Client Traces

image

This is a basic instrumentation of a fully hydrated page load. You'll find there's a sort-of hidden child here (with the (+) button):

image

Upon inspection, you'll see we effectively have an orphaned child. If this data were corerct, you'd expect a span generated on the server, wihch then expands into the client hydration. That span would also reflect the total duration of the client hydration. You would also expect the transaction here (the parent transaction - the server hydration) to reflect that total duration in some fashion.

Missing Loader Spans

image

Loaders are not capturing all relevant spans, and its currently unclear why. In this case we're calling out to an API endpoint in the loader, and that endpoint is using fetch (in Remix) to query another API service:

export async function loader({ params, context }: LoaderArgs) {
  invariant(params.bottleId);

  const bottle: BottleWithStats = await context.api.get(
    `/bottles/${params.bottleId}`,
  );

  return json({ bottle });
}

Missing TraceParent

image

In the same example above (the missing spans), traceparent is not present on any of the outgoing fetch calls, meaning the server never connects a trace.

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