Skip to content
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

Add support for Next.js cache instrumentation #12888

Open
AbhiPrasad opened this issue Jul 11, 2024 · 8 comments · May be fixed by #13124
Open

Add support for Next.js cache instrumentation #12888

AbhiPrasad opened this issue Jul 11, 2024 · 8 comments · May be fixed by #13124
Assignees
Labels
Feature: Spans Package: nextjs Issues related to the Sentry Nextjs SDK

Comments

@AbhiPrasad
Copy link
Member

Problem Statement

We support manually instrumenting caching, but this is a pain to figure out: https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/instrumentation/custom-instrumentation/caches-module/

Solution Brainstorm

We should either
a) auto-instrument the Next.js cache via https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath
b) update the cache module docs if auto-instrumentation is not possible.

@AbhiPrasad AbhiPrasad added Type: Improvement Package: nextjs Issues related to the Sentry Nextjs SDK labels Jul 11, 2024
@AbhiPrasad
Copy link
Member Author

@AbhiPrasad
Copy link
Member Author

asking in nextjs discussions about extending default cache handler: vercel/next.js#67677

@timfish
Copy link
Collaborator

timfish commented Jul 29, 2024

So some initial thoughts after a brief look...

We need to create some kind of cache wrapper that adds the instrumentation but this will only work for self-hosted. I guess hosted on Vercel is out of scope for this issue?

Passing the cacheHandler via require.resolve feels like a strange choice since it means that you can't just pass an already configured class instance 🤔. I need to check how the existing cache implementations accept their config and get to the bottom of why cacheHandler is a path before I can work out how wrapping this might work!

module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // disable default in-memory caching
}

I guess the default cache could be instrumented via require-in-the-middle of FileSystemCache 🤔

@timfish
Copy link
Collaborator

timfish commented Jul 29, 2024

...and whereas the Sentry docs warn about possible performance issues with JSON.stringify(data).length, the next.js code doesn't seem to have a problem with doing this, at least for the fetch cache:
https://github.com/vercel/next.js/blob/a0f83f8271a34cbd3fb531cffb7107aa8adb5e52/packages/next/src/server/lib/incremental-cache/index.ts#L504-L505

@timfish
Copy link
Collaborator

timfish commented Jul 29, 2024

Ok so it loads the cache implementation here:
https://github.com/vercel/next.js/blob/a0f83f8271a34cbd3fb531cffb7107aa8adb5e52/packages/next/src/export/helpers/create-incremental-cache.ts#L30-L34

  if (cacheHandler) {
    CacheHandler = interopDefault(
      await import(formatDynamicImportPath(dir, cacheHandler)).then(
        (mod) => mod.default || mod
      )
    )
  }

mod.default || mod isn't actually required because that's exactly what interopDefault does.

Passing around paths to modules like this is very inflexible so I'm hoping there's a good reason for it!

So if we offered a solution that wraps the handler at this config point:

  • It would need to return a path*
  • We'd need to find a way to pass the users cache handler path to us*
  • When we resolve the the users cache handler, we'd need to have the same absolute/relative resolution
    (* in a way the bundler picks up if that's required)

Assuming no bundler issues, the best I've got at this late hour is passing the user handler path via an environment variable 😂

module.exports = {
  cacheHandler: sentryCacheWrapper(require.resolve('./cache-handler.js')),
  cacheMaxMemorySize: 0, // disable default in-memory caching
}
export function sentryCacheWrapper(userHandler: string): string {
  process.env.__SENTRY_USER_CACHE_HANDLER_PATH = userHandler;
  return require.resolve('./sentry-cache-handler.cjs');
}

sentry-cache-handler.cjs

const userHandler = require(process.env.__SENTRY_USER_CACHE_HANDLER_PATH);

export default class SentryCacheHandler {
  function get(key) {
    // do instrumentation here, etc
    // return userHandler.get(key);
  }
}

It might be easier to use require-in-the-middle and wrap the IncrementalCache which is what wraps both the default cache, or any user supplied cache. I guess these are all internals so they might not be stable enough to do this.

@AbhiPrasad
Copy link
Member Author

JSON.stringify(data).length, the next.js code doesn't seem to have a problem with doing this, at least for the fetch cache

This would be worrying with arbitrary data (especially since stringifying certain primitives like booleans are not a good for the real world size), but because data is constrained to be IncrementalCacheValue, which seem to objects of fixed-key number, so should actually be pretty decently fast.

Though looking at https://github.com/vercel/next.js/blob/fd5581d7ef58e4e925485b8b3a2b30ea8a482732/packages/next/src/server/response-cache/types.ts#L87, I guess stringifying some of these fields buffer can get expensive. Maybe an LRU here? Would be interesting to profile IMO.

It might be easier to use require-in-the-middle and wrap the IncrementalCache which is what wraps both the default cache, or any user supplied cache. I guess these are all internals so they might not be stable enough to do this.

I'm leaning toward this option as long as we only wrap get and set - I don't think that part of the public API will change given this seems to be the public API surface of custom caches as well.

@lforst
Copy link
Member

lforst commented Jul 30, 2024

I think I like the approach of using require-in-the-middle but I would probably prefer wrapping the user's cache handler directly over wrapping IncrementalCache.

@timfish
Copy link
Collaborator

timfish commented Jul 30, 2024

I would probably prefer wrapping the user's cache handler directly over wrapping IncrementalCache

Yep, probably less prone to breakage and it looks like IncrementalCache is also used for the fetch cache

@timfish timfish linked a pull request Jul 30, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature: Spans Package: nextjs Issues related to the Sentry Nextjs SDK
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants