Skip to content

Commit

Permalink
feat(cloudflare): instrument scheduled handler
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Aug 2, 2024
1 parent 19bdb13 commit ac4c2b3
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 49 deletions.
46 changes: 44 additions & 2 deletions packages/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
},
};
```
5 changes: 2 additions & 3 deletions packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
58 changes: 55 additions & 3 deletions packages/cloudflare/src/handler.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -21,7 +28,7 @@ type ExtractEnv<P> = P extends ExportedHandler<infer Env> ? Env : never;
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withSentry<E extends ExportedHandler<any>>(
optionsCallback: (env: ExtractEnv<E>) => Options,
optionsCallback: (env: ExtractEnv<E>) => CloudflareOptions,
handler: E,
): E {
setAsyncLocalStorageAsyncContextStrategy();
Expand All @@ -40,5 +47,50 @@ export function withSentry<E extends ExportedHandler<any>>(
(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<ExportedHandlerScheduledHandler<ExtractEnv<E>>>) {
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<typeof target>);
} 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;
}
32 changes: 4 additions & 28 deletions packages/cloudflare/src/request.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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) });
}
29 changes: 29 additions & 0 deletions packages/cloudflare/src/scope-utils.ts
Original file line number Diff line number Diff line change
@@ -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) });
}
8 changes: 4 additions & 4 deletions packages/cloudflare/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ 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';
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(),
Expand All @@ -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);
}
Expand Down
19 changes: 10 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24347,10 +24347,10 @@ [email protected], mini-css-extract-plugin@^2.5.2:
dependencies:
schema-utils "^4.0.0"

[email protected].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==
[email protected].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"
Expand Down Expand Up @@ -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"
Expand All @@ -34130,14 +34130,15 @@ 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"
resolve.exports "^2.0.2"
selfsigned "^2.0.1"
source-map "^0.6.1"
unenv "npm:[email protected]"
workerd "1.20240718.0"
xxhash-wasm "^1.0.1"
optionalDependencies:
fsevents "~2.3.2"
Expand Down

0 comments on commit ac4c2b3

Please sign in to comment.