Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save smith/c1a6b29d98fdb89052b9790d816520a6 to your computer and use it in GitHub Desktop.
Save smith/c1a6b29d98fdb89052b9790d816520a6 to your computer and use it in GitHub Desktop.
Remix & Elastic APM distributed tracing correlation

We want to instrument Remix apps with the Elastic APM Node.js Agent on the server and the Elastic APM Real User Monitoring JavaScript Agent on the client.

We want the server-rendered page to have the trace id and other data available to the client, so we can correlate the traces between server and client. Elastic's distributed tracing docs explain this.

I've been able to make this work with Remix on Express, but it's super hacky, so this gist shows what I'm doing so hopefully somebody can point me to a better way to do this.

server/index.js

This is where we initialize the APM Node agent and Express. For APM, we just follow the instructions in the docs.

When we create request handler, we can use getLoadContext to put the configuration options we'll need for the RUM agent into the loader context. The loader context isn't necessarily where I want it, but this seems like the only way to pass data from the Express app into Remix.

return createRequestHandler({
          getLoadContext(req, res) {
            // this becomes the loader context
            return {
              apmRumAgentConfig: {
                serviceName: "remix-express-example-client",
                pageLoadSpanId: apm.currentTransaction.ensureParentId(),
                pageLoadTraceId: apm.currentTransaction.traceId,
                pageLoadSampled: apm.currentTransaction.sampled,
                serverUrl: apmServerUrl,
                serviceVersion: version,
              },
            };
          },
          build,
          mode: MODE,
        })(req, res, next);

app/routes/root.tsx

Since the data I need available in the loader context, I can load it via the root loader:

export const loader: LoaderFunction = ({ context }) => {
  return context;
};

This data should be available on every pageload.

app/entry.client.tsx

The best place to initialize the APM RUM agent on the client is here in entry.client.jsx.

We have access to the loader data here too, via the __remixContext global.

So, we can initialize the agent:

import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";

import { init as initApm } from "@elastic/apm-rum";
initApm(__remixContext.routeData.root.apmRumAgentConfig);

hydrate(<RemixBrowser />, document);

So now, I can look at a distributed trace in APM and see the client and the server connected:

CleanShot 2021-12-13 at 11 09 35@2x

But this is bad

The seems to work, but was a very convoluted process and seems like it would not be advised. I'd like to find a more elegant way to propagate these variables from the server to the client entry. Thanks for reading. Please help!

@smith
Copy link
Author

smith commented Jul 6, 2023

@depsimon I haven't revisited this since originally making it. I don't remember if this was a problem then or if it's something new.

The RUM agent exposes React Router components: https://www.elastic.co/guide/en/apm/agent/rum-js/4.x/react-integration.html#_instrumenting_application_routes

I'm not sure if it's required to use these components or if they work with how Remix uses React Router.

So, using the component from the RUM Agent might work, and if it doesn't I think you might need to instrument the route change events by customizing/configuring React Router.

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