feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(cloudflare): Add withSentry method - #13025

Merged
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry
Jul 29, 2024
Merged

feat(cloudflare): Add withSentry method#13025
AbhiPrasad merged 5 commits into
developfrom
abhi-cloudflare-with-sentry

Conversation

@AbhiPrasad

Copy link
Copy Markdown
Contributor

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*asSentryfrom'@sentry/cloudflare';exportdefaultwithSentry((env)=>({dsn: env.SENTRY_DSN,tracesSampleRate: 1.0,}),{asyncfetch(request,env,ctx){returnnewResponse('Hello World!');},}satisfiesExportedHandler<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 teamJuly 23, 2024 22:12
@AbhiPrasadAbhiPrasad self-assigned this Jul 23, 2024
@AbhiPrasad
AbhiPrasad requested review from andreiborza and lforst and removed request for a teamJuly 23, 2024 22:12
@github-actions

github-actionsBot commented Jul 23, 2024

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize
@sentry/browser22.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 flags63.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/react25.22 KB (+0.05% 🔺)
@sentry/react (incl. Tracing)37.22 KB (+0.03% 🔺)
@sentry/vue26.6 KB (+0.05% 🔺)
@sentry/vue (incl. Tracing)36.06 KB (+0.04% 🔺)
@sentry/svelte22.58 KB (+0.05% 🔺)
CDN Bundle23.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 - uncompressed69.37 KB (+0.07% 🔺)
CDN Bundle (incl. Tracing) - uncompressed106.31 KB (+0.02% 🔺)
CDN Bundle (incl. Tracing, Replay) - uncompressed217.95 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed230.78 KB (+0.01% 🔺)
@sentry/nextjs (client)37.08 KB (+0.02% 🔺)
@sentry/sveltekit (client)34.81 KB (+0.03% 🔺)
@sentry/node111.9 KB (0%)
@sentry/node - without tracing89.33 KB (-0.01% 🔽)
@sentry/aws-serverless98.49 KB (0%)

@AbhiPrasad
AbhiPrasad requested a review from mydeaJuly 24, 2024 13:44
Comment threadpackages/cloudflare/README.md Outdated
npm install @sentry/cloudflare
```

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

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadpackages/cloudflare/src/handler.ts Outdated
setHttpStatus(span, res.status);
return res;
} catch (e) {
captureException(e, { mechanism: { handled: false } });

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done in 5671b7b

@andreiborzaandreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nice!

@AbhiPrasad
AbhiPrasadforce-pushed the abhi-cloudflare-with-sentry branch from 87b4fa1 to e02e283CompareJuly 29, 2024 15:04
@AbhiPrasad
AbhiPrasad enabled auto-merge (squash) July 29, 2024 15:26
@AbhiPrasad
AbhiPrasad merged commit d2ab51c into developJul 29, 2024
@AbhiPrasad
AbhiPrasad deleted the abhi-cloudflare-with-sentry branch July 29, 2024 15:31
Sign up for freeto 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.

3 participants

@AbhiPrasad@chargome@andreiborza