Skip to content

feat(nextjs): Add server-side request transactions #3533

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

Merged

Conversation

lobsterkatie
Copy link
Member

@lobsterkatie lobsterkatie commented May 11, 2021

This PR adds tracing to requests for both page routes and API routes, by wrapping the server's main request handler. In order for the SDK to be initialized early enough for this to work, the logic for including the user's sentry.server.config.js file has changed in the following ways:

  • It is no longer injected into _document nor into individual API routes.
  • Instead, it's included as its own entry entry, which causes it to get output as a separate file, which we can then require earlier in the process.
  • As a result, _injectFile can be simplified, since it's now only handling client-side injection.

Why is having webpack output a separate file any better than the separate file it already is? The answer is that doing it this way runs it through the same webpack processors as the rest of the code written by the user, which is a good idea in general (and is what was happening when we were injecting it into existing entry entries), and which is specifically necessary in this case because (warning: a trip down into the weeds ahead):

  • We need to import or require sentry.server.config.js for its side effect of starting up the SDK.
  • We need the relative path to it to be resolved with respect to the user's nextjs project folder, not relative to our SDK, so we need to do the importing/requiring dynamically.
  • In order to use a dynamic import, we need to have a specific value for module in our tsconfig - and not only do we have a different one for our ESM builds, we've all seen how well it goes when we try to force our users into a particular TS configuration (see: localForage and its numerous associated issues and PRs), so that feels like a non-starter.
  • We therefore have to use require, but you can't require something which is itself using ESM imports, which our users easily might do in their sentry.server.config.js.
  • So, as a result of all of that, we need that file to be Babel-ized, and running it through next's webpack processors will do that.

Note: If you pull this to test it, you have to watch out for the relative path vs yarn linking problem, otherwise your test app won't build.

TODO in this PR:

  • Test this on Vercel (currently needs a beta release)
  • Add tests UPDATE: There's too much to mock here, so we'll make an example app against which to test in a separate PR

TODO in future PRs:

  • Use parameterized path for transaction name
  • Add request data to transaction
  • Deal with incoming sentry request headers (sentry-trace and tracestate)
  • Wrap routing code in a span? (We do this for Express)
  • Wrap Express middleware (which can be used with nextjs) in spans
  • Figure out if our Express integration fully covers the use of an Express server (which is also possible) or if modifications need to be made

@lobsterkatie lobsterkatie requested a review from kamilogorek as a code owner May 11, 2021 22:33
@github-actions
Copy link
Contributor

github-actions bot commented May 11, 2021

size-limit report

Path Size
@sentry/browser - CDN Bundle (gzipped) 20.64 KB (0%)
@sentry/browser - Webpack 21.52 KB (0%)
@sentry/react - Webpack 21.55 KB (0%)
@sentry/browser + @sentry/tracing - CDN Bundle (gzipped) 28.02 KB (0%)

@HazAT HazAT force-pushed the kmclb-next-js-add-request-transactions branch from 8cffa76 to b16c34a Compare May 12, 2021 07:17
// Each value in that object is either a string representing a single entry point, an array of such strings, or an
// object containing either of those, along with other configuration options. In that third case, the entry point(s) are
// listed under the key `import`.
type EntryPropertyObject = PlainObject<string | Array<string> | EntryPointObject>;
type EntryPropertyObject = PlainObject<string> | PlainObject<Array<string>> | PlainObject<EntryPointObject>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type EntryPropertyObject = PlainObject<string> | PlainObject<Array<string>> | PlainObject<EntryPointObject>;
type EntryPropertyObject = Record<string, string> | Record<string, Array<string>> | Record<string, EntryPointObject>;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should work as well, but you'd have to confirm whether it allows for the same property-based for in/of as [key: string] does. If so, then PlainObject can be removed entirely.

Copy link
Member Author

@lobsterkatie lobsterkatie May 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I'd learned that Record is really meant to be used on a known, finite set of keys. (I get that it would work - I'm talking about should.) Is that not true?

I made PlainObject just because I was tired of typing {[key: string] : whatever} a million times, and figured since there's Array their might as well be an equivalent for objects. ¯_(ツ)_/¯ I'm not committed to it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, no, I know it works. I just meant idiomatically. Here are a few things I dug out of my bookmarks:

https://stackoverflow.com/a/54100359/3874926

https://fnune.com/typescript/2019/01/30/typescript-series-1-record-is-usually-not-the-best-choice

https://rickcarlino.com/2017/02/27/real-world-use-case-for-typescript-record-types.html

https://stackoverflow.com/questions/51936369/what-is-the-record-type-in-typescript#comment103989203_51937036

Happy to go back to using { [key: string]: string } if you think it's better than PlainObject.

UPDATE: The playground hates it, but check out what compiles just fine if you actually use it in your code:

interface Object<T> extends ReturnType<ObjectConstructor> {
  [key: string]: T;
}

const numObject: Object<number> = {
  someNum: 4,
  someNonNum: 'whoops', // Error: Type 'string' is not assignable to type 'number'.
};

I get that it's a little unorthodox, but what do you think of using that?? 😁

@lobsterkatie lobsterkatie force-pushed the kmclb-next-js-add-request-transactions branch from b16c34a to c9f78ce Compare May 13, 2021 13:24
@lobsterkatie lobsterkatie requested a review from kamilogorek May 14, 2021 00:27

// The parent directory for the new temporary directory

describe('deepReadDirSync', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it 🥰

Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed comments!

@lobsterkatie lobsterkatie merged commit 594e5ca into kmclb-next-js-performance May 17, 2021
@lobsterkatie lobsterkatie deleted the kmclb-next-js-add-request-transactions branch May 17, 2021 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants