sentry-javascript icon indicating copy to clipboard operation
sentry-javascript copied to clipboard

Compatibility of @sentry/astro with Cloudflare Pages

Open mariusbolik opened this issue 1 year ago • 11 comments

Problem Statement

I installed the @sentry/astro integration in my Astro Project:

import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import sentry from "@sentry/astro";

export default defineConfig({
  output: "server",
  experimental: {
    actions: true,
  },
  adapter: cloudflare({}),
  integrations: [
    sentry({
      dsn: "https://18aba3b773c3ef1891e719e73a311ec4@o4505279164252160.ingest.us.sentry.io/4507712395214848",
      sourceMapsUploadOptions: {
        project: "pwa-astro",
        authToken: import.process.env.SENTRY_AUTH_TOKEN,
      },
    }),
  ],
});

But on build I receive this error:

11:16:47 [ERROR] [vite] x Build failed in 1.49s
[commonjs--resolver] [plugin vite:resolve] Cannot bundle Node.js built-in "node:http" imported from "node_modules/@sentry/node/build/esm/integrations/spotlight.js". Consider disabling ssr.noExternal or remove the built-in dependency.

So I extendet my astro config:

  vite: {
    ssr: {
      external: ["node:http"]
    },
  },

But then I get the next error:

[commonjs--resolver] [plugin vite:resolve] Cannot bundle Node.js built-in "node:fs" imported from "node_modules/@sentry/node/build/esm/integrations/contextlines.js". Consider disabling ssr.noExternal or remove the built-in dependency.

Solution Brainstorm

Cloudflare provides a sentry plugin: https://developers.cloudflare.com/pages/functions/plugins/sentry/ Is it possible that to use this plugin to make the Sentry/Astro integration compatible with CF Pages? Maybe releated to: https://github.com/getsentry/sentry-javascript/issues/12620

mariusbolik avatar Aug 03 '24 10:08 mariusbolik

Hello, thanks for reaching out.

Adding Cloudflare Pages support for Astro is on our roadmap and is actively being worked on. You can follow the progress in https://github.com/getsentry/sentry-javascript/issues/12620.

As for making the plugin work, have you already tried it? It's not maintained by us so a bit hard to verify 😅. Maybe my colleague @AbhiPrasad has a bit more insight into this.

andreiborza avatar Aug 05 '24 14:08 andreiborza

Thank you for coming back to me, @andreiborza. I tried the plugin, but it's not really compatible with the Astro middleware. The sentry plugin provided by cloudflare returns a response format that is not compatible with the response format that the astro middleware needs.

But I tried @sentry/cloudflare. It seems promissing so far. Sadly I also receive a typescript error. I don't know exactly how to change the requestHandlerOptions:

Image

Reproduction: https://stackblitz.com/edit/github-h3zdcy?file=src%2Fmiddleware.ts&on=stackblitz

mariusbolik avatar Aug 07 '24 14:08 mariusbolik

@AbhiPrasad seems like astro's middleware does not provide waitUntil and passThroughOnException functions, see https://docs.astro.build/en/guides/middleware/#the-context-object. Can we fall back to our own flush?

@mariusbolik this might be something to file with Astro as well.

andreiborza avatar Aug 08 '24 11:08 andreiborza

This should be passed through https://github.com/withastro/adapters/blob/cd4c0842aadc58defc67f4ccf6d6ef6f0401a9ac/packages/cloudflare/src/entrypoints/server.ts#L70

I guess it's called locals.runtime.ctx instead of context?

I'll be writing up a guide on this soon!

AbhiPrasad avatar Aug 08 '24 13:08 AbhiPrasad

Types are still a problem here, but this seems to work:

  const requestHandlerOptions = {
    options: {
      dsn: '',
      tracesSampleRate: 1.0,
    },
    request: context.request,
    context: (context.locals as any).runtime.ctx,
  };
  return Sentry.wrapRequestHandler(requestHandlerOptions, () => next());

Thanks @AbhiPrasad! Hopefully we can use the Astro/Sentry integration soon! I really like that it uploads the sourcemap files automatically :D

mariusbolik avatar Aug 09 '24 08:08 mariusbolik

@mariusbolik Thanks for the workaround!

const sentry = defineMiddleware(async (context, next) => {
  return wrapRequestHandler(
    {
      options: {
        dsn: import.meta.env.SENTRY_DSN,
        environment: import.meta.env.MODE,
        // tracesSampleRate: 1.0,
      },
      request: context.request,
      context: context.locals.runtime.ctx,
    },
    () => next()
  )
})

This works great using astro dev. 💥

However, astro build won't find waituntil when creating a production build (prior to deploying to Cloudflare Pages).

Cannot read properties of undefined (reading 'waitUntil')
  Stack trace:
    at file:///Users/mathias/devel/myapp/dist/_worker.js/_astro-internal_middleware.mjs:10133:23
    at async callMiddleware
...

Would you happen to have a solution for that too?

mlafeldt avatar Aug 26 '24 15:08 mlafeldt

Ok, it looks like adding something like this does the trick:

if (typeof context.locals.runtime.ctx?.waitUntil !== 'function') {
  return next()
}

mlafeldt avatar Aug 26 '24 16:08 mlafeldt

Hmm waitUntil should always be defined in the cloudflare runtime - let me dig in further to see why it's not being defined.

AbhiPrasad avatar Aug 26 '24 17:08 AbhiPrasad

Hmm waitUntil should always be defined in the cloudflare runtime - let me dig in further to see why it's not being defined.

It looks like context.locals.runtime.ctx is null when running astro build, which seems to be either a limitation of the Cloudflare adapter or Astro itself.

Another weirdness is that context.locals.runtime.env will include OS env vars with astro build, while it will only contain the variables from wrangler.toml and .dev.vars with astro dev. This leads to the unfortunate situation where you sometimes have to define env vars twice for some middlewares to work.

mlafeldt avatar Aug 27 '24 11:08 mlafeldt

We're still looking into this, thanks for the extra info.

andreiborza avatar Aug 28 '24 07:08 andreiborza

I can shed some light here, running astro build actually means that step is not running inside any Cloudflare runtime. Building the project is always running inside a node runtime, either locally or inside Cloudflare's build container, which is also Node.

Additionally this shouldn't be a big issue, but there is another caveat here. Middleware runs for prerendered (static generated) pages during astro build, and it runs in front of a request for on-demand (SSR) pages. There is no way to run a middleware during runtime for prerendered (static generated) pages as of today. Given that, it means that the Cloudflare context will not be available for prerendered (static generated) pages, since the middleware for them doesn't run in a Cloudflare runtime, not to mention it is not request scoped.

alexanderniebuhr avatar Aug 28 '24 13:08 alexanderniebuhr

@alexanderniebuhr thanks for providing the extra info. We'll take a look at this next week as some of us are currently out of the office.

andreiborza avatar Aug 29 '24 07:08 andreiborza

No problem, an additional note though, we are looking into adding a boolean value in context.prerender with Astro 5, which is currently in alpha. So that might need some time until there is a stable release, but that would allow for a check on a "official" context property value, instead of relying on a library or custom implementation to figure out in which context the middleware runs. We are also probably starting a RFC to make middleware more configurable, but that is very early ideas still. (Hope that helps a bit)

alexanderniebuhr avatar Aug 29 '24 09:08 alexanderniebuhr

@andreiborza Fixing wrapRequestHandler when executed as part of astro build could be as simple as this:

diff --git packages/cloudflare/src/request.ts packages/cloudflare/src/request.ts
index 560c17afb..e42e92ad1 100644
--- packages/cloudflare/src/request.ts
+++ packages/cloudflare/src/request.ts
@@ -89,7 +89,7 @@ export function wrapRequestHandler(
               captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
               throw e;
             } finally {
-              context.waitUntil(flush(2000));
+              context?.waitUntil(flush(2000));
             }
           },
         );

(context isn't used elsewhere)

mlafeldt avatar Aug 29 '24 12:08 mlafeldt

We'll take a look at this next week as some of us are currently out of the office.

No pressure, btw. Just sharing my findings. :)

mlafeldt avatar Aug 29 '24 13:08 mlafeldt

Thanks, I think from our POV it's also important that we do have some kind of flushing ability otherwise the function could be turned off before everything is flushed to Sentry.

andreiborza avatar Aug 29 '24 14:08 andreiborza

Thanks, I think from our POV it's also important that we do have some kind of flushing ability otherwise the function could be turned off before everything is flushed to Sentry.

context will only be undefined with astro build, so you can't use waitUntil anyway. I guess in that case, you could alternatively await the flush. But then again, when do users need to send anything to Sentry when prerendering Astro assets? :)

mlafeldt avatar Aug 29 '24 14:08 mlafeldt

I think generally, there's a case to be made that our SDKs don't have to run during prerendering but I'd argue this is a bigger case to look at. For now I opened #13549 to ensure we don't error when context is undefined for some reason.

Lms24 avatar Sep 02 '24 08:09 Lms24

@Lms24 Speaking of waitUntil, I noticed that Sentry.captureException won't work inside of it because the Sentry client injected via middleware might not be available in that scope. This happens, for example, when writing to D1:

Example:

Astro.locals.runtime.ctx.waitUntil(
  (async () => {
    const db = drizzle(Astro.locals.runtime.env.DB, { logger: true })
    await db.insert(usersTable).values(...)
      .catch((err) => {
        console.error(err)
        Sentry.captureException(err) // never captured because there's no Sentry client
      })
  })()
)

Sentry.captureException(...) // works fine outside of waitUntil

Logs:

Error: D1_ERROR: no such table: users: SQLITE_ERROR
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async eval (/Users/mathias/devel/myproject/src/pages/me.astro:31:9) {
  [cause]: Error: no such table: users: SQLITE_ERROR
      at D1Database._sendOrThrow (cloudflare-internal:d1-api:67:24)
      at async D1PreparedStatement.run (cloudflare-internal:d1-api:178:29) {
    [cause]: undefined
  }
}
Sentry Logger [warn]: No client configured on scope - will not capture exception!

What would be the best way to make the client available? Do I need to to construct a new one inside waitUntil perhaps?

mlafeldt avatar Sep 06 '24 12:09 mlafeldt

Hmm @AbhiPrasad any ideas? I'm not familiar enough with waitUntil unfortunately

Lms24 avatar Sep 06 '24 13:09 Lms24

Hmm @AbhiPrasad any ideas? I'm not familiar enough with waitUntil unfortunately

I would still appreciate any help here. @AbhiPrasad 🙏

Circling back to the original issue, I found out that @sentry/astro actually works on Cloudflare Pages incl. source map uploads with the following config:

export default defineConfig({
  integrations: [
    sentry({
      dsn: process.env.SENTRY_DSN,
      environment: import.meta.env.MODE,
      tracesSampleRate: 1.0,
      sourceMapsUploadOptions: {
        project: 'myproject',
        authToken: process.env.SENTRY_AUTH_TOKEN,
      },
      debug: true,
    }),
  ],
  output: 'hybrid',
  adapter: cloudflare({
    platformProxy: {
      enabled: true,
    },
    imageService: 'passthrough',
  }),
  vite: {
    ssr: {
      external: [
        'async_hooks',
        'diagnostics_channel',
        'events',
        'module',
        'node:child_process',
        'node:fs',
        'node:http',
        'node:https',
        'node:inspector',
        'node:net',
        'node:os',
        'node:path',
        'node:readline',
        'node:stream',
        'node:tls',
        'node:util',
        'node:worker_threads',
        'node:zlib',
        'path',
        'url',
        'util',
        'worker_threads',
      ],
    },
    build: {
      minify: false,
      sourcemap: true,
    },
  },
})

However, I had to patch node_modules/@sentry/browser/build/npm/esm/transports/fetch.js to remove referrerPolicy: 'origin' to fix this error:

Sentry Logger [error]: Error while sending event: Error: The 'referrerPolicy' field on 'RequestInitializerDict' is not implemented.

This suggests that the Sentry code somehow thinks it's running in a browser and not on the server.

Anyway, capturing traces and exceptions works, so proper CF support seems pretty close.

mlafeldt avatar Sep 11 '24 13:09 mlafeldt

Last but not least, you can also combine these two options:

  • Use Sentry.wrapRequestHandler from @sentry/cloudflare in middleware.ts
  • Set up the Sentry Vite plugin for source map uploads (simply add this to your Astro config)

Which doesn't involve any hacks at all.

mlafeldt avatar Sep 11 '24 15:09 mlafeldt

@mlafeldt thanks for the workarounds/discussion, we'll take a look at get back to you.

andreiborza avatar Sep 12 '24 01:09 andreiborza

Last but not least, you can also combine these two options:

  • Use Sentry.wrapRequestHandler from @sentry/cloudflare in middleware.ts
  • Set up the Sentry Vite plugin for source map uploads (simply add this to your Astro config)

Which doesn't involve any hacks at all.

How did you manage to setup @sentry/cloudflare with Astro? Here is how i did it https://github.com/rodilo/sentry-cloudflare-middleware-test/blob/main/src/middleware/sentry-middleware.ts

App is built and deployed on cloudflare, but sentry doesn't seem to work this way. I've added a console.log in the middleware to check if ctx object has waitUntil and passThroughOnException but they were not there, so i guess it has something to do with that. When i build application locally on my computer, those functions are there. I've also deployed locally build app to cloudflare with wrangler and sentry integration worked.

Any help is appreciated.

rodilo avatar Oct 01 '24 13:10 rodilo

@rodilo Your middleware looks fine to me. I'd double-check the setup locally using wrangler pages dev - see https://docs.astro.build/en/guides/deploy/cloudflare/#enabling-preview-locally-with-wrangler (that whole guide is worth a read). I can't tell where you deploy the project (Cloudflare Pages?), but I'd check that ASTRO_SERVER_SENTRY_DSN is set in CI too.

mlafeldt avatar Oct 01 '24 19:10 mlafeldt

@rodilo Your middleware looks fine to me. I'd double-check the setup locally using wrangler pages dev - see https://docs.astro.build/en/guides/deploy/cloudflare/#enabling-preview-locally-with-wrangler (that whole guide is worth a read). I can't tell where you deploy the project (Cloudflare Pages?), but I'd check that ASTRO_SERVER_SENTRY_DSN is set in CI too.

In the end it was ASTRO_SERVER_SENTRY_DSN missing in my original repo, forgot to add it to astro config 🙈. Thanks for helping me realize that.

rodilo avatar Oct 02 '24 09:10 rodilo

So we do have https://docs.sentry.io/platforms/javascript/guides/cloudflare/frameworks/astro/, which is not ideal because there's a bunch of manual steps but it gets stuff working for you.

We'll keep improving this, but closing this for now.

AbhiPrasad avatar Nov 01 '24 16:11 AbhiPrasad

@AbhiPrasad I tried following the documentation with no success. How are you supposed to get that middleware working? AFAIK, Astro Cloudflare adapter does not support functions. Do you have a working repo?

masylum avatar Dec 04 '24 09:12 masylum

@masylum this works for me:

// eslint-disable-next-line import/no-extraneous-dependencies
import { wrapRequestHandler } from '@sentry/cloudflare';
import { defineMiddleware } from 'astro/middleware';

export const sentryMiddleware = defineMiddleware(async (context, next) =>
	wrapRequestHandler(
		{
			options: {
				dsn: import.meta.env.SENTRY_DSN,
				environment: import.meta.env.PUBLIC_ENVIRONMENT,
			},
			// @ts-expect-error Type for request from Astro and Cloudflare differ slightly as not all cf properties are available in Astro's type
			request: context.request,
			context: (context.locals as any).runtime.ctx,
		},
		() => next()
	)
);

rodilo avatar Dec 04 '24 09:12 rodilo

Awesome! I tried it and it works. Perhaps it would be worth updating that Sentry documentation since it's misleading. Next step: upload the source maps 😆

masylum avatar Dec 04 '24 10:12 masylum