diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md
index 7c7512e2ed1d..1dd73d7c3da9 100644
--- a/packages/cloudflare/README.md
+++ b/packages/cloudflare/README.md
@@ -15,7 +15,7 @@
- [Official SDK Docs](https://docs.sentry.io/quickstart/)
- [TypeDoc](http://getsentry.github.io/sentry-javascript/)
-**Note: This SDK is unreleased. Please follow the
+**Note: This SDK is in an alpha state. Please follow the
[tracking GH issue](https://github.com/getsentry/sentry-javascript/issues/12620) for updates.**
## Install
@@ -143,8 +143,50 @@ You can use the `instrumentD1WithSentry` method to instrument [Cloudflare D1](ht
Cloudflare's serverless SQL database with Sentry.
```javascript
+import * as Sentry from '@sentry/cloudflare';
+
// env.DB is the D1 DB binding configured in your `wrangler.toml`
-const db = instrumentD1WithSentry(env.DB);
+const db = Sentry.instrumentD1WithSentry(env.DB);
// Now you can use the database as usual
await db.prepare('SELECT * FROM table WHERE id = ?').bind(1).run();
```
+
+## Cron Monitoring (Cloudflare Workers)
+
+[Sentry Crons](https://docs.sentry.io/product/crons/) allows you to monitor the uptime and performance of any scheduled,
+recurring job in your application.
+
+To instrument your cron triggers, use the `Sentry.withMonitor` API in your
+[`Scheduled` handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/).
+
+```js
+export default {
+ async scheduled(event, env, ctx) {
+ ctx.waitUntil(
+ Sentry.withMonitor('your-cron-name', () => {
+ return doSomeTaskOnASchedule();
+ }),
+ );
+ },
+};
+```
+
+You can also use supply a monitor config to upsert cron monitors with additional metadata:
+
+```js
+const monitorConfig = {
+ schedule: {
+ type: 'crontab',
+ value: '* * * * *',
+ },
+ checkinMargin: 2, // In minutes. Optional.
+ maxRuntime: 10, // In minutes. Optional.
+ timezone: 'America/Los_Angeles', // Optional.
+};
+
+export default {
+ async scheduled(event, env, ctx) {
+ Sentry.withMonitor('your-cron-name', () => doSomeTaskOnASchedule(), monitorConfig);
+ },
+};
+```
diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json
index 2b77932537f1..c80dfa758efe 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -47,10 +47,9 @@
"@cloudflare/workers-types": "^4.x"
},
"devDependencies": {
- "@cloudflare/workers-types": "^4.20240722.0",
+ "@cloudflare/workers-types": "^4.20240725.0",
"@types/node": "^14.18.0",
- "miniflare": "^3.20240718.0",
- "wrangler": "^3.65.1"
+ "wrangler": "^3.67.1"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts
index 65f3cf8bcbf1..1533e902638a 100644
--- a/packages/cloudflare/src/handler.ts
+++ b/packages/cloudflare/src/handler.ts
@@ -1,7 +1,14 @@
-import type { ExportedHandler, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
-import type { Options } from '@sentry/types';
+import type {
+ ExportedHandler,
+ ExportedHandlerFetchHandler,
+ ExportedHandlerScheduledHandler,
+} from '@cloudflare/workers-types';
+import { captureException, flush, startSpan, withIsolationScope } from '@sentry/core';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
+import type { CloudflareOptions } from './client';
import { wrapRequestHandler } from './request';
+import { addCloudResourceContext } from './scope-utils';
+import { init } from './sdk';
/**
* Extract environment generic from exported handler.
@@ -21,7 +28,7 @@ type ExtractEnv
= P extends ExportedHandler ? Env : never;
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withSentry>(
- optionsCallback: (env: ExtractEnv) => Options,
+ optionsCallback: (env: ExtractEnv) => CloudflareOptions,
handler: E,
): E {
setAsyncLocalStorageAsyncContextStrategy();
@@ -40,5 +47,50 @@ export function withSentry>(
(handler.fetch as any).__SENTRY_INSTRUMENTED__ = true;
}
+ if (
+ 'scheduled' in handler &&
+ typeof handler.scheduled === 'function' &&
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ !(handler.scheduled as any).__SENTRY_INSTRUMENTED__
+ ) {
+ handler.scheduled = new Proxy(handler.scheduled, {
+ apply(target, thisArg, args: Parameters>>) {
+ const [event, env, context] = args;
+ return withIsolationScope(isolationScope => {
+ const options = optionsCallback(env);
+ const client = init(options);
+ isolationScope.setClient(client);
+
+ addCloudResourceContext(isolationScope);
+
+ return startSpan(
+ {
+ op: 'faas.cron',
+ name: `Scheduled Cron ${event.cron}`,
+ attributes: {
+ 'faas.cron': event.cron,
+ 'faas.time': new Date(event.scheduledTime).toISOString(),
+ 'faas.trigger': 'timer',
+ },
+ },
+ async () => {
+ try {
+ return await (target.apply(thisArg, args) as ReturnType);
+ } catch (e) {
+ captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
+ throw e;
+ } finally {
+ context.waitUntil(flush(2000));
+ }
+ },
+ );
+ });
+ },
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ (handler.scheduled as any).__SENTRY_INSTRUMENTED__ = true;
+ }
+
return handler;
}
diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts
index b10037ec8bc0..c6cfaf8d8bbf 100644
--- a/packages/cloudflare/src/request.ts
+++ b/packages/cloudflare/src/request.ts
@@ -1,4 +1,4 @@
-import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
+import type { ExecutionContext, IncomingRequestCfProperties, Request, Response } from '@cloudflare/workers-types';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
@@ -11,9 +11,10 @@ import {
startSpan,
withIsolationScope,
} from '@sentry/core';
-import type { Scope, SpanAttributes } from '@sentry/types';
-import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils';
+import type { SpanAttributes } from '@sentry/types';
+import { stripUrlQueryAndFragment } from '@sentry/utils';
import type { CloudflareOptions } from './client';
+import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
interface RequestHandlerWrapperOptions {
@@ -96,28 +97,3 @@ export function wrapRequestHandler(
);
});
}
-
-/**
- * Set cloud resource context on scope.
- */
-function addCloudResourceContext(scope: Scope): void {
- scope.setContext('cloud_resource', {
- 'cloud.provider': 'cloudflare',
- });
-}
-
-/**
- * Set culture context on scope
- */
-function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void {
- scope.setContext('culture', {
- timezone: cf.timezone,
- });
-}
-
-/**
- * Set request data on scope
- */
-function addRequest(scope: Scope, request: Request): void {
- scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
-}
diff --git a/packages/cloudflare/src/scope-utils.ts b/packages/cloudflare/src/scope-utils.ts
new file mode 100644
index 000000000000..1f5bbce8f0fc
--- /dev/null
+++ b/packages/cloudflare/src/scope-utils.ts
@@ -0,0 +1,29 @@
+import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
+
+import type { Scope } from '@sentry/types';
+import { winterCGRequestToRequestData } from '@sentry/utils';
+
+/**
+ * Set cloud resource context on scope.
+ */
+export function addCloudResourceContext(scope: Scope): void {
+ scope.setContext('cloud_resource', {
+ 'cloud.provider': 'cloudflare',
+ });
+}
+
+/**
+ * Set culture context on scope
+ */
+export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void {
+ scope.setContext('culture', {
+ timezone: cf.timezone,
+ });
+}
+
+/**
+ * Set request data on scope
+ */
+export function addRequest(scope: Scope, request: Request): void {
+ scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
+}
diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts
index ca2035388c12..a16a9e578a06 100644
--- a/packages/cloudflare/src/sdk.ts
+++ b/packages/cloudflare/src/sdk.ts
@@ -7,9 +7,9 @@ import {
linkedErrorsIntegration,
requestDataIntegration,
} from '@sentry/core';
-import type { Integration, Options } from '@sentry/types';
+import type { Integration } from '@sentry/types';
import { stackParserFromStackParserOptions } from '@sentry/utils';
-import type { CloudflareClientOptions } from './client';
+import type { CloudflareClientOptions, CloudflareOptions } from './client';
import { CloudflareClient } from './client';
import { fetchIntegration } from './integrations/fetch';
@@ -17,7 +17,7 @@ import { makeCloudflareTransport } from './transport';
import { defaultStackParser } from './vendor/stacktrace';
/** Get the default integrations for the Cloudflare SDK. */
-export function getDefaultIntegrations(options: Options): Integration[] {
+export function getDefaultIntegrations(options: CloudflareOptions): Integration[] {
const sendDefaultPii = options.sendDefaultPii ?? false;
return [
dedupeIntegration(),
@@ -32,7 +32,7 @@ export function getDefaultIntegrations(options: Options): Integration[] {
/**
* Initializes the cloudflare SDK.
*/
-export function init(options: Options): CloudflareClient | undefined {
+export function init(options: CloudflareOptions): CloudflareClient | undefined {
if (options.defaultIntegrations === undefined) {
options.defaultIntegrations = getDefaultIntegrations(options);
}
diff --git a/yarn.lock b/yarn.lock
index 2f570de66ef9..953f0eee5f3a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -24347,10 +24347,10 @@ mini-css-extract-plugin@2.6.1, mini-css-extract-plugin@^2.5.2:
dependencies:
schema-utils "^4.0.0"
-miniflare@3.20240718.0, miniflare@^3.20240718.0:
- version "3.20240718.0"
- resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.0.tgz#41561c6620b2b15803f5b3d2e903ed3af40f3b0b"
- integrity sha512-TKgSeyqPBeT8TBLxbDJOKPWlq/wydoJRHjAyDdgxbw59N6wbP8JucK6AU1vXCfu21eKhrEin77ssXOpbfekzPA==
+miniflare@3.20240718.1:
+ version "3.20240718.1"
+ resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.1.tgz#26ccb95be087cd99cd478dbf2e3a3d40f231bf45"
+ integrity sha512-mn3MjGnpgYvarCRTfz4TQyVyY8yW0zz7f8LOAPVai78IGC/lcVcyskZcuIr7Zovb2i+IERmmsJAiEPeZHIIKbA==
dependencies:
"@cspotcode/source-map-support" "0.8.1"
acorn "^8.8.0"
@@ -34118,10 +34118,10 @@ workerpool@^6.4.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462"
integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A==
-wrangler@^3.65.1:
- version "3.65.1"
- resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.65.1.tgz#493bd92b504f9f056cd57bbe2d430797600c914b"
- integrity sha512-Z5NyrbpGMQCpim/6VnI1im0/Weh5+CU1sdep1JbfFxHjn/Jt9K+MeUq+kCns5ubkkdRx2EYsusB/JKyX2JdJ4w==
+wrangler@^3.67.1:
+ version "3.67.1"
+ resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.67.1.tgz#c9bb344b70c8c2106ad33f03beaa063dd5b49526"
+ integrity sha512-lLVJxq/OZMfntvZ79WQJNC1OKfxOCs6PLfogqDBuPFEQ3L/Mwqvd9IZ0bB8ahrwUN/K3lSdDPXynk9HfcGZxVw==
dependencies:
"@cloudflare/kv-asset-handler" "0.3.4"
"@esbuild-plugins/node-globals-polyfill" "^0.2.3"
@@ -34130,7 +34130,7 @@ wrangler@^3.65.1:
chokidar "^3.5.3"
date-fns "^3.6.0"
esbuild "0.17.19"
- miniflare "3.20240718.0"
+ miniflare "3.20240718.1"
nanoid "^3.3.3"
path-to-regexp "^6.2.0"
resolve "^1.22.8"
@@ -34138,6 +34138,7 @@ wrangler@^3.65.1:
selfsigned "^2.0.1"
source-map "^0.6.1"
unenv "npm:unenv-nightly@1.10.0-1717606461.a117952"
+ workerd "1.20240718.0"
xxhash-wasm "^1.0.1"
optionalDependencies:
fsevents "~2.3.2"