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"