Skip to content

Commit 6aa304f

Browse files
committed
feat(cloudflare): instrument scheduled handler
1 parent 945cdbc commit 6aa304f

File tree

8 files changed

+150
-49
lines changed

8 files changed

+150
-49
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"postpublish": "lerna run --stream --concurrency 1 postpublish",
3434
"test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test",
3535
"test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit",
36-
"test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,profiling-node,serverless,google-cloud,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"",
36+
"test-ci-browser": "lerna run test --ignore \"@sentry/{bun,cloudflare,deno,gatsby,google-cloud,nextjs,node,profiling-node,remix,serverless,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"",
3737
"test-ci-node": "ts-node ./scripts/node-unit-tests.ts",
3838
"test-ci-bun": "lerna run test --scope @sentry/bun",
3939
"test:update-snapshots": "lerna run test:update-snapshots",

Diff for: packages/cloudflare/README.md

+45-1
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
@@ -136,3 +136,47 @@ Sentry.captureEvent({
136136
],
137137
});
138138
```
139+
140+
## Cron Monitoring (Cloudflare Workers)
141+
142+
[Sentry Crons](https://docs.sentry.io/product/crons/) allows you to monitor the uptime and performance of any scheduled,
143+
recurring job in your application.
144+
145+
To instrument your cron triggers, use the `Sentry.withMonitor` API in your
146+
[`Scheduled` handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/).
147+
148+
```js
149+
export default {
150+
async scheduled(event, env, ctx) {
151+
Sentry.withMonitor('your-cron-name', () => {
152+
ctx.waitUntil(doSomeTaskOnASchedule());
153+
});
154+
},
155+
};
156+
```
157+
158+
You can also use supply a monitor config to upsert cron monitors with additional metadata:
159+
160+
```js
161+
const monitorConfig = {
162+
schedule: {
163+
type: 'crontab',
164+
value: '* * * * *',
165+
},
166+
checkinMargin: 2, // In minutes. Optional.
167+
maxRuntime: 10, // In minutes. Optional.
168+
timezone: 'America/Los_Angeles', // Optional.
169+
};
170+
171+
export default {
172+
async scheduled(event, env, ctx) {
173+
Sentry.withMonitor(
174+
'your-cron-name',
175+
() => {
176+
ctx.waitUntil(doSomeTaskOnASchedule());
177+
},
178+
monitorConfig,
179+
);
180+
},
181+
};
182+
```

Diff for: 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",

Diff for: packages/cloudflare/src/handler.ts

+55-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
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 { captureException, flush, startSpan, withIsolationScope } from '@sentry/core';
37
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
8+
import type { CloudflareOptions } from './client';
49
import { wrapRequestHandler } from './request';
10+
import { addCloudResourceContext } from './scope-utils';
11+
import { init } from './sdk';
512

613
/**
714
* Extract environment generic from exported handler.
@@ -21,7 +28,7 @@ type ExtractEnv<P> = P extends ExportedHandler<infer Env> ? Env : never;
2128
*/
2229
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2330
export function withSentry<E extends ExportedHandler<any>>(
24-
optionsCallback: (env: ExtractEnv<E>) => Options,
31+
optionsCallback: (env: ExtractEnv<E>) => CloudflareOptions,
2532
handler: E,
2633
): E {
2734
setAsyncLocalStorageAsyncContextStrategy();
@@ -40,5 +47,50 @@ export function withSentry<E extends ExportedHandler<any>>(
4047
(handler.fetch as any).__SENTRY_INSTRUMENTED__ = true;
4148
}
4249

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

Diff for: packages/cloudflare/src/request.ts

+4-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
1+
import type { ExecutionContext, IncomingRequestCfProperties, Request, Response } from '@cloudflare/workers-types';
22

33
import {
44
SEMANTIC_ATTRIBUTE_SENTRY_OP,
@@ -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-
}

Diff for: packages/cloudflare/src/scope-utils.ts

+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+
}

Diff for: packages/cloudflare/src/sdk.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ import {
77
linkedErrorsIntegration,
88
requestDataIntegration,
99
} from '@sentry/core';
10-
import type { Integration, Options } from '@sentry/types';
10+
import type { Integration } from '@sentry/types';
1111
import { stackParserFromStackParserOptions } from '@sentry/utils';
12-
import type { CloudflareClientOptions } from './client';
12+
import type { CloudflareClientOptions, CloudflareOptions } from './client';
1313
import { CloudflareClient } from './client';
1414

1515
import { fetchIntegration } from './integrations/fetch';
1616
import { makeCloudflareTransport } from './transport';
1717
import { defaultStackParser } from './vendor/stacktrace';
1818

1919
/** Get the default integrations for the Cloudflare SDK. */
20-
export function getDefaultIntegrations(options: Options): Integration[] {
20+
export function getDefaultIntegrations(options: CloudflareOptions): Integration[] {
2121
const sendDefaultPii = options.sendDefaultPii ?? false;
2222
return [
2323
dedupeIntegration(),
@@ -32,7 +32,7 @@ export function getDefaultIntegrations(options: Options): Integration[] {
3232
/**
3333
* Initializes the cloudflare SDK.
3434
*/
35-
export function init(options: Options): CloudflareClient | undefined {
35+
export function init(options: CloudflareOptions): CloudflareClient | undefined {
3636
if (options.defaultIntegrations === undefined) {
3737
options.defaultIntegrations = getDefaultIntegrations(options);
3838
}

Diff for: yarn.lock

+10-9
Original file line numberDiff line numberDiff line change
@@ -24153,10 +24153,10 @@ [email protected], mini-css-extract-plugin@^2.5.2:
2415324153
dependencies:
2415424154
schema-utils "^4.0.0"
2415524155

24156-
[email protected].0, miniflare@^3.20240718.0:
24157-
version "3.20240718.0"
24158-
resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.0.tgz#41561c6620b2b15803f5b3d2e903ed3af40f3b0b"
24159-
integrity sha512-TKgSeyqPBeT8TBLxbDJOKPWlq/wydoJRHjAyDdgxbw59N6wbP8JucK6AU1vXCfu21eKhrEin77ssXOpbfekzPA==
24156+
24157+
version "3.20240718.1"
24158+
resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.1.tgz#26ccb95be087cd99cd478dbf2e3a3d40f231bf45"
24159+
integrity sha512-mn3MjGnpgYvarCRTfz4TQyVyY8yW0zz7f8LOAPVai78IGC/lcVcyskZcuIr7Zovb2i+IERmmsJAiEPeZHIIKbA==
2416024160
dependencies:
2416124161
"@cspotcode/source-map-support" "0.8.1"
2416224162
acorn "^8.8.0"
@@ -33877,10 +33877,10 @@ workerpool@^6.4.0:
3387733877
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462"
3387833878
integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A==
3387933879

33880-
wrangler@^3.65.1:
33881-
version "3.65.1"
33882-
resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.65.1.tgz#493bd92b504f9f056cd57bbe2d430797600c914b"
33883-
integrity sha512-Z5NyrbpGMQCpim/6VnI1im0/Weh5+CU1sdep1JbfFxHjn/Jt9K+MeUq+kCns5ubkkdRx2EYsusB/JKyX2JdJ4w==
33880+
wrangler@^3.67.1:
33881+
version "3.67.1"
33882+
resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.67.1.tgz#c9bb344b70c8c2106ad33f03beaa063dd5b49526"
33883+
integrity sha512-lLVJxq/OZMfntvZ79WQJNC1OKfxOCs6PLfogqDBuPFEQ3L/Mwqvd9IZ0bB8ahrwUN/K3lSdDPXynk9HfcGZxVw==
3388433884
dependencies:
3388533885
"@cloudflare/kv-asset-handler" "0.3.4"
3388633886
"@esbuild-plugins/node-globals-polyfill" "^0.2.3"
@@ -33889,14 +33889,15 @@ wrangler@^3.65.1:
3388933889
chokidar "^3.5.3"
3389033890
date-fns "^3.6.0"
3389133891
esbuild "0.17.19"
33892-
miniflare "3.20240718.0"
33892+
miniflare "3.20240718.1"
3389333893
nanoid "^3.3.3"
3389433894
path-to-regexp "^6.2.0"
3389533895
resolve "^1.22.8"
3389633896
resolve.exports "^2.0.2"
3389733897
selfsigned "^2.0.1"
3389833898
source-map "^0.6.1"
3389933899
unenv "npm:[email protected]"
33900+
workerd "1.20240718.0"
3390033901
xxhash-wasm "^1.0.1"
3390133902
optionalDependencies:
3390233903
fsevents "~2.3.2"

0 commit comments

Comments
 (0)