Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(cloudflare): Add withSentry method #13025

Merged
merged 5 commits into from
Jul 29, 2024
Merged

Conversation

AbhiPrasad
Copy link
Member

ref #12620

before reviewing this PR, I recommend reading through a writeup I did: #13007

This PR adds withSentry, a method that wraps your cloudflare worker handler to add Sentry instrumentation. The writeup above explains why we need to do this over just a regular Sentry.init call.

The implementation of withSentry is fairly straightforward, wrapping the fetch handler in the cloudflare worker with:

  1. withIsolationScope to isolate it from other concurrent requests
  2. helpers to update scope with relevant contexts/request
  3. continueTrace to continue distributed tracing
  4. startSpan to track spans

Usage looks something like so:

import * as Sentry from '@sentry/cloudflare';

export default withSentry(
  (env) => ({
    dsn: env.SENTRY_DSN,
    tracesSampleRate: 1.0,
  }),
  {
    async fetch(request, env, ctx) {
      return new Response('Hello World!');
    },
  } satisfies ExportedHandler<Env>,
);

image

image

Next step here is to add more robust e2e tests, and then release an initial version!

@AbhiPrasad AbhiPrasad requested a review from a team July 23, 2024 22:12
@AbhiPrasad AbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad AbhiPrasad requested review from lforst and andreiborza and removed request for a team July 23, 2024 22:12
Copy link
Contributor

github-actions bot commented Jul 23, 2024

size-limit report 📦

Path Size
@sentry/browser 22.45 KB (+0.06% 🔺)
@sentry/browser (incl. Tracing) 34.22 KB (+0.02% 🔺)
@sentry/browser (incl. Tracing, Replay) 70.26 KB (+0.02% 🔺)
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 63.59 KB (+0.02% 🔺)
@sentry/browser (incl. Tracing, Replay with Canvas) 74.66 KB (+0.02% 🔺)
@sentry/browser (incl. Tracing, Replay, Feedback) 87.24 KB (+0.02% 🔺)
@sentry/browser (incl. Tracing, Replay, Feedback, metrics) 89.08 KB (+0.02% 🔺)
@sentry/browser (incl. metrics) 26.75 KB (+0.06% 🔺)
@sentry/browser (incl. Feedback) 39.37 KB (+0.03% 🔺)
@sentry/browser (incl. sendFeedback) 27.06 KB (+0.04% 🔺)
@sentry/browser (incl. FeedbackAsync) 31.7 KB (+0.04% 🔺)
@sentry/react 25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing) 37.22 KB (+0.03% 🔺)
@sentry/vue 26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing) 36.06 KB (+0.04% 🔺)
@sentry/svelte 22.58 KB (+0.05% 🔺)
CDN Bundle 23.64 KB (+0.14% 🔺)
CDN Bundle (incl. Tracing) 35.88 KB (+0.03% 🔺)
CDN Bundle (incl. Tracing, Replay) 70.26 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) 75.53 KB (+0.02% 🔺)
CDN Bundle - uncompressed 69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed 106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed 217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 230.78 KB (+0.01% 🔺)
@sentry/nextjs (client) 37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client) 34.81 KB (+0.03% 🔺)
@sentry/node 111.9 KB (0%)
@sentry/node - without tracing 89.33 KB (-0.01% 🔽)
@sentry/aws-serverless 98.49 KB (0%)

@AbhiPrasad AbhiPrasad requested a review from mydea July 24, 2024 13:44
npm install @sentry/cloudflare
```

Then set either the `nodejs_compat` or `nodejs_als` compatibility flags in your `wrangler.toml`:
Copy link
Member

Choose a reason for hiding this comment

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

l: Could we add a sentence of why this is needed? I saw Because we require tracing support as top-of-mind priority, AsyncLocalStorage support is a requirement of the SDK. in the issue you made.

Copy link
Member Author

Choose a reason for hiding this comment

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

done in 5671b7b

Comment on lines +77 to +98
// Set user information, as well as tags and further extras
Sentry.setExtra('battery', 0.7);
Sentry.setTag('user_mode', 'admin');
Sentry.setUser({ id: '4711' });

// Add a breadcrumb for future events
Sentry.addBreadcrumb({
message: 'My Breadcrumb',
// ...
});

// Capture exceptions, messages or manual events
Sentry.captureMessage('Hello, world!');
Sentry.captureException(new Error('Good bye'));
Sentry.captureEvent({
message: 'Manual',
stacktrace: [
// ...
],
});
Copy link
Member

Choose a reason for hiding this comment

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

q: From your issue:

This needs to be done because workers can change their bindings (env) without re-deploying the entire worker, so a top-level Sentry.init call would become stale.

Wouldn't these suffer from the same issue of going stale?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, because under the hood they call getClient() which should always have the current client in the handler execution context, therefore will stay up-to-date.

setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });
Copy link
Member

Choose a reason for hiding this comment

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

q: I've seen some sdks add additional keys to mechanism, e.g. type: 'bun', do we want that here too? why or why not?

Copy link
Member Author

Choose a reason for hiding this comment

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

As per the develop docs

Required unique identifier of this mechanism determining rendering and processing of the mechanism data.

In the Python SDK this is merely the name of the framework integration that produced the exception, while for native it is e.g. "minidump" or "applecrashreport".

Changed with 309c05a

@@ -0,0 +1,300 @@
// Note: These tests run the handler in Node.js, which is has some differences to the cloudflare workers runtime.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Note: These tests run the handler in Node.js, which is has some differences to the cloudflare workers runtime.
// Note: These tests run the handler in Node.js, which has some differences to the cloudflare workers runtime.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed with 61ad09f

Comment on lines 72 to 83
const handler = {
async fetch(_request, _env, _context) {
expect(SentryCore.getClient() instanceof CloudflareClient).toBe(true);
return new Response('test');
},
} satisfies ExportedHandler;

const context = createMockExecutionContext();
const wrappedHandler = withSentry(() => ({}), handler);
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, context);

expect.assertions(1);
Copy link
Member

Choose a reason for hiding this comment

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

m: I think a robuster way to write this is to create a vi.fn that gets called with SentryCore.getClient() and assert that it has been called with that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call!

Changed with 61ad09f

Comment on lines +88 to +106
const handler = {
async fetch(_request, _env, _context) {
SentryCore.captureMessage('test');
return new Response('test');
},
} satisfies ExportedHandler;

let sentryEvent: Event = {};
const wrappedHandler = withSentry(
(env: any) => ({
dsn: env.MOCK_DSN,
beforeSend(event) {
sentryEvent = event;
return null;
},
}),
handler,
);
Copy link
Member

Choose a reason for hiding this comment

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

l: can we extract this into a helper just call that in the next few tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll come back to take a look at this after this PR merges, good suggestion.

import * as SentryCore from '@sentry/core';
import { init } from '../src/sdk';

describe('init', () => {
Copy link
Member

Choose a reason for hiding this comment

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

m: could we add a test for init returning a client?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed with 87b4fa1

export default withSentry(
(env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add a comment explaining this value?

Copy link
Member Author

Choose a reason for hiding this comment

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

done in 5671b7b

Copy link
Member

@andreiborza andreiborza left a comment

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad AbhiPrasad force-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283 Compare July 29, 2024 15:04
@AbhiPrasad AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad AbhiPrasad merged commit d2ab51c into develop Jul 29, 2024
127 checks passed
@AbhiPrasad AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants