Skip to content

Commit 945cdbc

Browse files
authored
feat(cloudflare): Add plugin for cloudflare pages (#13123)
Before reviewing this change, I recommend reading through a GH discussion I wrote up that explains the reasoning behind the API surface of the cloudflare SDK: #13007 This PR adds support for [Cloudflare Pages](https://developers.cloudflare.com/pages/), Cloudflare's fullstack development deployment platform that is powered by Cloudflare Workers under the hood. Think of this platform having very similar capabilities (and constraints) as Vercel. To set the plugin up, you do something like so: ```javascript // functions/_middleware.js import * as Sentry from '@sentry/cloudflare'; export const onRequest = Sentry.sentryPagesPlugin({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, }); ``` We have to use the middleware instead of a global init because we need to call `init` for every single new incoming request to make sure the sentry instance does not get stale with redeployments. While implementing `sentryPagesPlugin`, I noticed that there was a logic that was redundant between it and `withSentry`, the API for cloudflare workers. This led me to refactor this into a common helper, `wrapRequestHandler`, which is contained in `packages/cloudflare/src/request.ts`. That is why there is diffs in this PR for `packages/cloudflare/src/handler.ts`.
1 parent e2668f8 commit 945cdbc

10 files changed

+543
-355
lines changed

CHANGELOG.md

+22
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@
99

1010
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1111

12+
## Unreleased
13+
14+
### Important Changes
15+
16+
- **feat(cloudflare): Add plugin for cloudflare pages (#13123)**
17+
18+
This release adds support for Cloudflare Pages to `@sentry/cloudflare`, our SDK for the
19+
[Cloudflare Workers JavaScript Runtime](https://developers.cloudflare.com/workers/)! For details on how to use it,
20+
please see the [README](./packages/cloudflare/README.md). Any feedback/bug reports are greatly appreciated, please
21+
[reach out on GitHub](https://github.com/getsentry/sentry-javascript/issues/12620).
22+
23+
```javascript
24+
// functions/_middleware.js
25+
import * as Sentry from '@sentry/cloudflare';
26+
27+
export const onRequest = Sentry.sentryPagesPlugin({
28+
dsn: __PUBLIC_DSN__,
29+
// Set tracesSampleRate to 1.0 to capture 100% of spans for tracing.
30+
tracesSampleRate: 1.0,
31+
});
32+
```
33+
1234
## 8.21.0
1335

1436
### Important Changes

packages/cloudflare/README.md

+46-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
</a>
55
</p>
66

7-
# Official Sentry SDK for Cloudflare [UNRELEASED]
7+
# Official Sentry SDK for Cloudflare
88

99
[![npm version](https://img.shields.io/npm/v/@sentry/cloudflare.svg)](https://www.npmjs.com/package/@sentry/cloudflare)
1010
[![npm dm](https://img.shields.io/npm/dm/@sentry/cloudflare.svg)](https://www.npmjs.com/package/@sentry/cloudflare)
@@ -18,9 +18,7 @@
1818
**Note: This SDK is unreleased. Please follow the
1919
[tracking GH issue](https://github.com/getsentry/sentry-javascript/issues/12620) for updates.**
2020

21-
Below details the setup for the Cloudflare Workers. Cloudflare Pages support is in active development.
22-
23-
## Setup (Cloudflare Workers)
21+
## Install
2422

2523
To get started, first install the `@sentry/cloudflare` package:
2624

@@ -36,6 +34,46 @@ compatibility_flags = ["nodejs_compat"]
3634
# compatibility_flags = ["nodejs_als"]
3735
```
3836

37+
Then you can either setup up the SDK for [Cloudflare Pages](#setup-cloudflare-pages) or
38+
[Cloudflare Workers](#setup-cloudflare-workers).
39+
40+
## Setup (Cloudflare Pages)
41+
42+
To use this SDK, add the `sentryPagesPlugin` as
43+
[middleware to your Cloudflare Pages application](https://developers.cloudflare.com/pages/functions/middleware/).
44+
45+
We recommend adding a `functions/_middleware.js` for the middleware setup so that Sentry is initialized for your entire
46+
app.
47+
48+
```javascript
49+
// functions/_middleware.js
50+
import * as Sentry from '@sentry/cloudflare';
51+
52+
export const onRequest = Sentry.sentryPagesPlugin({
53+
dsn: process.env.SENTRY_DSN,
54+
// Set tracesSampleRate to 1.0 to capture 100% of spans for tracing.
55+
tracesSampleRate: 1.0,
56+
});
57+
```
58+
59+
If you need to to chain multiple middlewares, you can do so by exporting an array of middlewares. Make sure the Sentry
60+
middleware is the first one in the array.
61+
62+
```javascript
63+
import * as Sentry from '@sentry/cloudflare';
64+
65+
export const onRequest = [
66+
// Make sure Sentry is the first middleware
67+
Sentry.sentryPagesPlugin({
68+
dsn: process.env.SENTRY_DSN,
69+
tracesSampleRate: 1.0,
70+
}),
71+
// Add more middlewares here
72+
];
73+
```
74+
75+
## Setup (Cloudflare Workers)
76+
3977
To use this SDK, wrap your handler with the `withSentry` function. This will initialize the SDK and hook into the
4078
environment. Note that you can turn off almost all side effects using the respective options.
4179

@@ -58,7 +96,7 @@ export default withSentry(
5896
);
5997
```
6098

61-
### Sourcemaps (Cloudflare Workers)
99+
### Sourcemaps
62100

63101
Configure uploading sourcemaps via the Sentry Wizard:
64102

@@ -68,10 +106,11 @@ npx @sentry/wizard@latest -i sourcemaps
68106

69107
See more details in our [docs](https://docs.sentry.io/platforms/javascript/sourcemaps/).
70108

71-
## Usage (Cloudflare Workers)
109+
## Usage
72110

73111
To set context information or send manual events, use the exported functions of `@sentry/cloudflare`. Note that these
74-
functions will require your exported handler to be wrapped in `withSentry`.
112+
functions will require the usage of the Sentry helpers, either `withSentry` function for Cloudflare Workers or the
113+
`sentryPagesPlugin` middleware for Cloudflare Pages.
75114

76115
```javascript
77116
import * as Sentry from '@sentry/cloudflare';

packages/cloudflare/src/handler.ts

+5-99
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,7 @@
1-
import type {
2-
ExportedHandler,
3-
ExportedHandlerFetchHandler,
4-
IncomingRequestCfProperties,
5-
} from '@cloudflare/workers-types';
6-
import {
7-
SEMANTIC_ATTRIBUTE_SENTRY_OP,
8-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
9-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
10-
captureException,
11-
continueTrace,
12-
flush,
13-
setHttpStatus,
14-
startSpan,
15-
withIsolationScope,
16-
} from '@sentry/core';
17-
import type { Options, Scope, SpanAttributes } from '@sentry/types';
18-
import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils';
1+
import type { ExportedHandler, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
2+
import type { Options } from '@sentry/types';
193
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
20-
import { init } from './sdk';
4+
import { wrapRequestHandler } from './request';
215

226
/**
237
* Extract environment generic from exported handler.
@@ -47,70 +31,8 @@ export function withSentry<E extends ExportedHandler<any>>(
4731
handler.fetch = new Proxy(handler.fetch, {
4832
apply(target, thisArg, args: Parameters<ExportedHandlerFetchHandler<ExtractEnv<E>>>) {
4933
const [request, env, context] = args;
50-
return withIsolationScope(isolationScope => {
51-
const options = optionsCallback(env);
52-
const client = init(options);
53-
isolationScope.setClient(client);
54-
55-
const attributes: SpanAttributes = {
56-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.cloudflare-worker',
57-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
58-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
59-
['http.request.method']: request.method,
60-
['url.full']: request.url,
61-
};
62-
63-
const contentLength = request.headers.get('content-length');
64-
if (contentLength) {
65-
attributes['http.request.body.size'] = parseInt(contentLength, 10);
66-
}
67-
68-
let pathname = '';
69-
try {
70-
const url = new URL(request.url);
71-
pathname = url.pathname;
72-
attributes['server.address'] = url.hostname;
73-
attributes['url.scheme'] = url.protocol.replace(':', '');
74-
} catch {
75-
// skip
76-
}
77-
78-
addRequest(isolationScope, request);
79-
addCloudResourceContext(isolationScope);
80-
if (request.cf) {
81-
addCultureContext(isolationScope, request.cf);
82-
attributes['network.protocol.name'] = request.cf.httpProtocol;
83-
}
84-
85-
const routeName = `${request.method} ${pathname ? stripUrlQueryAndFragment(pathname) : '/'}`;
86-
87-
return continueTrace(
88-
{ sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') },
89-
() => {
90-
// Note: This span will not have a duration unless I/O happens in the handler. This is
91-
// because of how the cloudflare workers runtime works.
92-
// See: https://developers.cloudflare.com/workers/runtime-apis/performance/
93-
return startSpan(
94-
{
95-
name: routeName,
96-
attributes,
97-
},
98-
async span => {
99-
try {
100-
const res = await (target.apply(thisArg, args) as ReturnType<typeof target>);
101-
setHttpStatus(span, res.status);
102-
return res;
103-
} catch (e) {
104-
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
105-
throw e;
106-
} finally {
107-
context.waitUntil(flush(2000));
108-
}
109-
},
110-
);
111-
},
112-
);
113-
});
34+
const options = optionsCallback(env);
35+
return wrapRequestHandler({ options, request, context }, () => target.apply(thisArg, args));
11436
},
11537
});
11638

@@ -120,19 +42,3 @@ export function withSentry<E extends ExportedHandler<any>>(
12042

12143
return handler;
12244
}
123-
124-
function addCloudResourceContext(isolationScope: Scope): void {
125-
isolationScope.setContext('cloud_resource', {
126-
'cloud.provider': 'cloudflare',
127-
});
128-
}
129-
130-
function addCultureContext(isolationScope: Scope, cf: IncomingRequestCfProperties): void {
131-
isolationScope.setContext('culture', {
132-
timezone: cf.timezone,
133-
});
134-
}
135-
136-
function addRequest(isolationScope: Scope, request: Request): void {
137-
isolationScope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
138-
}

packages/cloudflare/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export {
8585
} from '@sentry/core';
8686

8787
export { withSentry } from './handler';
88+
export { sentryPagesPlugin } from './pages-plugin';
8889

8990
export { CloudflareClient } from './client';
9091
export { getDefaultIntegrations } from './sdk';
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
2+
import type { CloudflareOptions } from './client';
3+
import { wrapRequestHandler } from './request';
4+
5+
/**
6+
* Plugin middleware for Cloudflare Pages.
7+
*
8+
* Initializes the SDK and wraps cloudflare pages requests with SDK instrumentation.
9+
*
10+
* @example
11+
* ```javascript
12+
* // functions/_middleware.js
13+
* import * as Sentry from '@sentry/cloudflare';
14+
*
15+
* export const onRequest = Sentry.sentryPagesPlugin({
16+
* dsn: process.env.SENTRY_DSN,
17+
* tracesSampleRate: 1.0,
18+
* });
19+
* ```
20+
*
21+
* @param _options
22+
* @returns
23+
*/
24+
export function sentryPagesPlugin<
25+
Env = unknown,
26+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27+
Params extends string = any,
28+
Data extends Record<string, unknown> = Record<string, unknown>,
29+
>(options: CloudflareOptions): PagesPluginFunction<Env, Params, Data, CloudflareOptions> {
30+
setAsyncLocalStorageAsyncContextStrategy();
31+
return context => wrapRequestHandler({ options, request: context.request, context }, () => context.next());
32+
}

0 commit comments

Comments
 (0)