nuxt-community / sentry-module

Sentry module for Nuxt 2
https://sentry.nuxtjs.org
MIT License
500 stars 113 forks source link

Replay data isn't available to tunnel middleware if Sentry tunnel is enabled #562

Closed ryan-cahill closed 1 year ago

ryan-cahill commented 1 year ago

Version

@nuxtjs/sentry: 7.2.1 nuxt: 2.16.1

Sentry configuration

publicRuntimeConfig: {
  ...
      sentry: {
        config: {
          dsn: '<removed>',
          ...
        },
        clientConfig: {
          tunnel: '/tunnel/sentry',
        },
      },
    },
...
    sentry: {
      dsn: '<removed>',
      clientConfig: {
        replaysSessionSampleRate: process.env.ENVIRONMENT === 'local' ? 1.0 : 0.1,
        replaysOnErrorSampleRate: 1.0,
      },
      clientIntegrations: {
        Replay: {},
      },
    },
    ....
    serverMiddleware: [
      { path: '/tunnel/sentry', handler: 'server-middleware/sentry.tunnel.ts' },
    ]
    ...
  ...

Sentry server middleware

// src/server-middleware/sentry.tunnel.ts

import axios from 'axios';
import express from 'express';

// https://github.com/getsentry/examples/blob/a9f50106988ce2d21e41ab8584ebaccef4988709/tunneling/nextjs/pages/api/tunnel.js

// Change host appropriately if you run your own Sentry instance.
const sentryHost = '.sentry.io';

// Set knownProjectIds to an array with your Sentry project IDs which you
// want to accept through this proxy.
const knownProjectIds = ['<removed>'];

const middleware = express();

middleware.use(express.text({ limit: '50mb' }));

middleware.all('/', async (req, res) => {
  try {
    const envelope = req.body;

    const pieces = envelope.split('\n');

    const header = JSON.parse(pieces[0]);

    const { host, pathname } = new URL(header.dsn);
    if (!host.endsWith(sentryHost)) {
      throw new Error(`invalid host: ${host}`);
    }

    const projectId = pathname.slice(1);
    if (!knownProjectIds.includes(projectId)) {
      throw new Error(`invalid project id: ${projectId}`);
    }

    const url = `https://${host}/api/${projectId}/envelope/`;
    const response = await axios.post(url, envelope, { timeout: 10000 });
    res.end(JSON.stringify(response.data));
  } catch (e) {
    console.error(e);
    res.statusCode = 400;
    res.end(JSON.stringify({ status: 'invalid request' }));
  }
});

export default middleware;

Steps to reproduce

What is Expected?

There should be some kind of data representing the replay event in req.body in the tunnel middleware to forward to Sentry

What is actually happening?

req.body is just an empty object in the tunnel middleware, so there's no replay data to forward to Sentry

rchl commented 1 year ago

It hardly looks like a problem with this module.

I would guess that Sentry payload doesn't have text/* type so express.text probably is not parsing it. You should probably inspect the request that Sentry makes on the client side and adjust the parsing middleware so that it handles that type of payload.

rchl commented 1 year ago

Based on some fix I did in sentry-testkit, maybe a middleware like bodyParser.text({ type: () => true }) would be more effective. See https://github.com/zivl/sentry-testkit/blob/d5d0685e561f7bd3ed387c745365ef64593eb36b/src/localServerApi.ts#L23-L25

ryan-cahill commented 1 year ago

Thanks for the suggestion @rchl. That did seem to get the issue closer to being resolved, but there was still a missing piece that I didn't figure out. Seeing as I'm just evaluating the Session Replay feature, I didn't spend too much time continuing down that road. I found that the setting below solved the issue for now and Session Replay worked using the tunnel as expected. I found this from a similar issue in another repo

// nuxt.config.ts
...
      clientIntegrations: {
        Replay: {
          useCompression: false,
...
rchl commented 1 year ago

As explained by @lforst in the linked issue:

Hi, this probably doesn't work because replay uses a binary format for it's payloads and nextjs automatically bodyparses.

express.text is essentially body-parser so same issue applies here.

I would suggest trying @nuxtjs/proxy module instead of a custom server middleware.

Something like this might work:

  modules: [
    '@nuxtjs/proxy',
    '@nuxtjs/sentry',
  ],
  proxy: {
    '/tunnel/sentry': {
      target: 'https://[xxx].ingest.sentry.io/api/[projectId]/envelope/',
      ignorePath: true,
    },
  },
  // ...
}

Of course the [projectId] and [xxx] needs to be replaced with actual data from the DSN.

(This setup is not as dynamic as the one suggested by Sentry where Sentry origin and project ID is parsed automatically from the request URL but it might be good enough.)