Skip to content

Commit c7ac38c

Browse files
committed
feat(cloudflare): Add plugin for cloudflare pages
1 parent e2668f8 commit c7ac38c

File tree

10 files changed

+543
-355
lines changed

10 files changed

+543
-355
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
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

Lines changed: 46 additions & 7 deletions
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

Lines changed: 5 additions & 99 deletions
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

Lines changed: 1 addition & 0 deletions
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';
Lines changed: 32 additions & 0 deletions
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)