Skip to content

Commit 6e4ee13

Browse files
authored
Merge branch 'develop' into abhi-fix-fastify-errors
2 parents 0459800 + c558ecb commit 6e4ee13

File tree

16 files changed

+478
-106
lines changed

16 files changed

+478
-106
lines changed

.craft.yml

+2
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ targets:
183183
format: base64
184184
'npm:@sentry/bun':
185185
onlyIfPresent: /^sentry-bun-\d.*\.tgz$/
186+
'npm:@sentry/cloudflare':
187+
onlyIfPresent: /^sentry-cloudflare-\d.*\.tgz$/
186188
'npm:@sentry/deno':
187189
onlyIfPresent: /^sentry-deno-\d.*\.tgz$/
188190
'npm:@sentry/ember':

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

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

12+
Work in this release was contributed by @horochx. Thank you for your contribution!
13+
1214
## 8.22.0
1315

1416
### Important Changes

dev-packages/browser-integration-tests/utils/staticAssets.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ export function addStaticAssetSymlink(localOutPath: string, originalPath: string
2727

2828
// Only copy files once
2929
if (!fs.existsSync(newPath)) {
30-
fs.symlinkSync(originalPath, newPath);
30+
try {
31+
fs.symlinkSync(originalPath, newPath);
32+
} catch (error) {
33+
// There must be some race condition here as some of our tests flakey
34+
// because the file already exists. Let's catch and ignore
35+
// only ignore these kind of errors
36+
if (!`${error}`.includes('file already exists')) {
37+
throw error;
38+
}
39+
}
3140
}
3241

3342
symlinkAsset(newPath, path.join(localOutPath, fileName));

dev-packages/node-integration-tests/suites/express/without-tracing/test.ts

+1-18
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,7 @@ test('correctly applies isolation scope even without tracing', done => {
2323
},
2424
},
2525
})
26-
.expect({
27-
event: {
28-
transaction: 'GET /test/isolationScope/2',
29-
tags: {
30-
global: 'tag',
31-
'isolation-scope': 'tag',
32-
'isolation-scope-2': '2',
33-
},
34-
// Request is correctly set
35-
request: {
36-
url: expect.stringContaining('/test/isolationScope/2'),
37-
headers: {
38-
'user-agent': expect.stringContaining(''),
39-
},
40-
},
41-
},
42-
})
4326
.start(done);
4427

45-
runner.makeRequest('get', '/test/isolationScope/1').then(() => runner.makeRequest('get', '/test/isolationScope/2'));
28+
runner.makeRequest('get', '/test/isolationScope/1');
4629
});

packages/cloudflare/README.md

+75-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
- [Official SDK Docs](https://docs.sentry.io/quickstart/)
1616
- [TypeDoc](http://getsentry.github.io/sentry-javascript/)
1717

18-
**Note: This SDK is unreleased. Please follow the
18+
**Note: This SDK is in an alpha state. Please follow the
1919
[tracking GH issue](https://github.com/getsentry/sentry-javascript/issues/12620) for updates.**
2020

2121
## Install
@@ -72,6 +72,37 @@ export const onRequest = [
7272
];
7373
```
7474

75+
If you need to access the `context` object (for example to grab environmental variables), you can pass a function to
76+
`sentryPagesPlugin` that takes the `context` object as an argument and returns `init` options:
77+
78+
```javascript
79+
export const onRequest = Sentry.sentryPagesPlugin(context => ({
80+
dsn: context.env.SENTRY_DSN,
81+
tracesSampleRate: 1.0,
82+
}));
83+
```
84+
85+
If you do not have access to the `onRequest` middleware API, you can use the `wrapRequestHandler` API instead.
86+
87+
Here is an example with SvelteKit:
88+
89+
```javascript
90+
// hooks.server.js
91+
import * as Sentry from '@sentry/cloudflare';
92+
93+
export const handle = ({ event, resolve }) => {
94+
const requestHandlerOptions = {
95+
options: {
96+
dsn: event.platform.env.SENTRY_DSN,
97+
tracesSampleRate: 1.0,
98+
},
99+
request: event.request,
100+
context: event.platform.ctx,
101+
};
102+
return Sentry.wrapRequestHandler(requestHandlerOptions, () => resolve(event));
103+
};
104+
```
105+
75106
## Setup (Cloudflare Workers)
76107

77108
To use this SDK, wrap your handler with the `withSentry` function. This will initialize the SDK and hook into the
@@ -143,8 +174,50 @@ You can use the `instrumentD1WithSentry` method to instrument [Cloudflare D1](ht
143174
Cloudflare's serverless SQL database with Sentry.
144175

145176
```javascript
177+
import * as Sentry from '@sentry/cloudflare';
178+
146179
// env.DB is the D1 DB binding configured in your `wrangler.toml`
147-
const db = instrumentD1WithSentry(env.DB);
180+
const db = Sentry.instrumentD1WithSentry(env.DB);
148181
// Now you can use the database as usual
149182
await db.prepare('SELECT * FROM table WHERE id = ?').bind(1).run();
150183
```
184+
185+
## Cron Monitoring (Cloudflare Workers)
186+
187+
[Sentry Crons](https://docs.sentry.io/product/crons/) allows you to monitor the uptime and performance of any scheduled,
188+
recurring job in your application.
189+
190+
To instrument your cron triggers, use the `Sentry.withMonitor` API in your
191+
[`Scheduled` handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/).
192+
193+
```js
194+
export default {
195+
async scheduled(event, env, ctx) {
196+
ctx.waitUntil(
197+
Sentry.withMonitor('your-cron-name', () => {
198+
return doSomeTaskOnASchedule();
199+
}),
200+
);
201+
},
202+
};
203+
```
204+
205+
You can also use supply a monitor config to upsert cron monitors with additional metadata:
206+
207+
```js
208+
const monitorConfig = {
209+
schedule: {
210+
type: 'crontab',
211+
value: '* * * * *',
212+
},
213+
checkinMargin: 2, // In minutes. Optional.
214+
maxRuntime: 10, // In minutes. Optional.
215+
timezone: 'America/Los_Angeles', // Optional.
216+
};
217+
218+
export default {
219+
async scheduled(event, env, ctx) {
220+
Sentry.withMonitor('your-cron-name', () => doSomeTaskOnASchedule(), monitorConfig);
221+
},
222+
};
223+
```

packages/cloudflare/package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,9 @@
4747
"@cloudflare/workers-types": "^4.x"
4848
},
4949
"devDependencies": {
50-
"@cloudflare/workers-types": "^4.20240722.0",
50+
"@cloudflare/workers-types": "^4.20240725.0",
5151
"@types/node": "^14.18.0",
52-
"miniflare": "^3.20240718.0",
53-
"wrangler": "^3.65.1"
52+
"wrangler": "^3.67.1"
5453
},
5554
"scripts": {
5655
"build": "run-p build:transpile build:types",

packages/cloudflare/src/handler.ts

+64-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
import type { ExportedHandler, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
2-
import type { Options } from '@sentry/types';
1+
import type {
2+
ExportedHandler,
3+
ExportedHandlerFetchHandler,
4+
ExportedHandlerScheduledHandler,
5+
} from '@cloudflare/workers-types';
6+
import {
7+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
8+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
9+
captureException,
10+
flush,
11+
startSpan,
12+
withIsolationScope,
13+
} from '@sentry/core';
314
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
15+
import type { CloudflareOptions } from './client';
416
import { wrapRequestHandler } from './request';
17+
import { addCloudResourceContext } from './scope-utils';
18+
import { init } from './sdk';
519

620
/**
721
* Extract environment generic from exported handler.
@@ -21,7 +35,7 @@ type ExtractEnv<P> = P extends ExportedHandler<infer Env> ? Env : never;
2135
*/
2236
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2337
export function withSentry<E extends ExportedHandler<any>>(
24-
optionsCallback: (env: ExtractEnv<E>) => Options,
38+
optionsCallback: (env: ExtractEnv<E>) => CloudflareOptions,
2539
handler: E,
2640
): E {
2741
setAsyncLocalStorageAsyncContextStrategy();
@@ -40,5 +54,52 @@ export function withSentry<E extends ExportedHandler<any>>(
4054
(handler.fetch as any).__SENTRY_INSTRUMENTED__ = true;
4155
}
4256

57+
if (
58+
'scheduled' in handler &&
59+
typeof handler.scheduled === 'function' &&
60+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
61+
!(handler.scheduled as any).__SENTRY_INSTRUMENTED__
62+
) {
63+
handler.scheduled = new Proxy(handler.scheduled, {
64+
apply(target, thisArg, args: Parameters<ExportedHandlerScheduledHandler<ExtractEnv<E>>>) {
65+
const [event, env, context] = args;
66+
return withIsolationScope(isolationScope => {
67+
const options = optionsCallback(env);
68+
const client = init(options);
69+
isolationScope.setClient(client);
70+
71+
addCloudResourceContext(isolationScope);
72+
73+
return startSpan(
74+
{
75+
op: 'faas.cron',
76+
name: `Scheduled Cron ${event.cron}`,
77+
attributes: {
78+
'faas.cron': event.cron,
79+
'faas.time': new Date(event.scheduledTime).toISOString(),
80+
'faas.trigger': 'timer',
81+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare',
82+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
83+
},
84+
},
85+
async () => {
86+
try {
87+
return await (target.apply(thisArg, args) as ReturnType<typeof target>);
88+
} catch (e) {
89+
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
90+
throw e;
91+
} finally {
92+
context.waitUntil(flush(2000));
93+
}
94+
},
95+
);
96+
});
97+
},
98+
});
99+
100+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
101+
(handler.scheduled as any).__SENTRY_INSTRUMENTED__ = true;
102+
}
103+
43104
return handler;
44105
}

packages/cloudflare/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export {
8888
export { withSentry } from './handler';
8989
export { sentryPagesPlugin } from './pages-plugin';
9090

91+
export { wrapRequestHandler } from './request';
92+
9193
export { CloudflareClient } from './client';
9294
export { getDefaultIntegrations } from './sdk';
9395

packages/cloudflare/src/pages-plugin.ts

+30-5
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,48 @@ import { wrapRequestHandler } from './request';
77
*
88
* Initializes the SDK and wraps cloudflare pages requests with SDK instrumentation.
99
*
10-
* @example
10+
* @example Simple usage
11+
*
1112
* ```javascript
1213
* // functions/_middleware.js
1314
* import * as Sentry from '@sentry/cloudflare';
1415
*
1516
* export const onRequest = Sentry.sentryPagesPlugin({
16-
* dsn: process.env.SENTRY_DSN,
17-
* tracesSampleRate: 1.0,
17+
* dsn: process.env.SENTRY_DSN,
18+
* tracesSampleRate: 1.0,
1819
* });
1920
* ```
21+
*
22+
* @example Usage with handler function to access context for environmental variables
23+
*
24+
* ```javascript
25+
* import * as Sentry from '@sentry/cloudflare';
26+
*
27+
* const const onRequest = Sentry.sentryPagesPlugin((context) => ({
28+
* dsn: context.env.SENTRY_DSN,
29+
* tracesSampleRate: 1.0,
30+
* })
31+
* ```
32+
*
33+
* @param handlerOrOptions Configuration options or a function that returns configuration options.
34+
* @returns A plugin function that can be used in Cloudflare Pages.
2035
*/
2136
export function sentryPagesPlugin<
2237
Env = unknown,
2338
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2439
Params extends string = any,
2540
Data extends Record<string, unknown> = Record<string, unknown>,
26-
>(options: CloudflareOptions): PagesPluginFunction<Env, Params, Data, CloudflareOptions> {
41+
// Although it is not ideal to use `any` here, it makes usage more flexible for different setups.
42+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
43+
PluginParams = any,
44+
>(
45+
handlerOrOptions:
46+
| CloudflareOptions
47+
| ((context: EventPluginContext<Env, Params, Data, PluginParams>) => CloudflareOptions),
48+
): PagesPluginFunction<Env, Params, Data, PluginParams> {
2749
setAsyncLocalStorageAsyncContextStrategy();
28-
return context => wrapRequestHandler({ options, request: context.request, context }, () => context.next());
50+
return context => {
51+
const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
52+
return wrapRequestHandler({ options, request: context.request, context }, () => context.next());
53+
};
2954
}

packages/cloudflare/src/request.ts

+3-27
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
startSpan,
1212
withIsolationScope,
1313
} from '@sentry/core';
14-
import type { Scope, SpanAttributes } from '@sentry/types';
15-
import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils';
14+
import type { SpanAttributes } from '@sentry/types';
15+
import { stripUrlQueryAndFragment } from '@sentry/utils';
1616
import type { CloudflareOptions } from './client';
17+
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
1718
import { init } from './sdk';
1819

1920
interface RequestHandlerWrapperOptions {
@@ -96,28 +97,3 @@ export function wrapRequestHandler(
9697
);
9798
});
9899
}
99-
100-
/**
101-
* Set cloud resource context on scope.
102-
*/
103-
function addCloudResourceContext(scope: Scope): void {
104-
scope.setContext('cloud_resource', {
105-
'cloud.provider': 'cloudflare',
106-
});
107-
}
108-
109-
/**
110-
* Set culture context on scope
111-
*/
112-
function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void {
113-
scope.setContext('culture', {
114-
timezone: cf.timezone,
115-
});
116-
}
117-
118-
/**
119-
* Set request data on scope
120-
*/
121-
function addRequest(scope: Scope, request: Request): void {
122-
scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
123-
}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
2+
3+
import type { Scope } from '@sentry/types';
4+
import { winterCGRequestToRequestData } from '@sentry/utils';
5+
6+
/**
7+
* Set cloud resource context on scope.
8+
*/
9+
export function addCloudResourceContext(scope: Scope): void {
10+
scope.setContext('cloud_resource', {
11+
'cloud.provider': 'cloudflare',
12+
});
13+
}
14+
15+
/**
16+
* Set culture context on scope
17+
*/
18+
export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void {
19+
scope.setContext('culture', {
20+
timezone: cf.timezone,
21+
});
22+
}
23+
24+
/**
25+
* Set request data on scope
26+
*/
27+
export function addRequest(scope: Scope, request: Request): void {
28+
scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
29+
}

0 commit comments

Comments
 (0)