Skip to content

Commit f2afa29

Browse files
authored
feat(cloudflare): Add support for email, queue, and tail handler (#16233)
Cloudflare workers have a variety of handlers you can define for different functionality: https://developers.cloudflare.com/workers/runtime-apis/handlers/ Right now our instrumentation wraps the `fetch` and `scheduled` handlers. This PR extends our instrumentation to also wrap the `queue`, `email`, and `tail` handler. We only create spans for queue and email, but not for tail because its meant as a debugging/analytics endpoint. We can introduce spans in `tail` handler if there is demand in the future. We need to add this wrapping, otherwise users cannot use the SDK in these handlers because of how the cloudflare isolation model works (fetch handler is completed isolated from email and queue handlers, so we need to wrap every handler individually).
1 parent befe970 commit f2afa29

File tree

2 files changed

+803
-18
lines changed

2 files changed

+803
-18
lines changed

packages/cloudflare/src/handler.ts

+117-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
captureException,
33
flush,
4+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
45
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
56
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
67
startSpan,
@@ -66,7 +67,7 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
6667
'faas.cron': event.cron,
6768
'faas.time': new Date(event.scheduledTime).toISOString(),
6869
'faas.trigger': 'timer',
69-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare',
70+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.scheduled',
7071
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
7172
},
7273
},
@@ -87,8 +88,122 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
8788

8889
markAsInstrumented(handler.scheduled);
8990
}
91+
92+
if ('email' in handler && typeof handler.email === 'function' && !isInstrumented(handler.email)) {
93+
handler.email = new Proxy(handler.email, {
94+
apply(target, thisArg, args: Parameters<EmailExportedHandler<Env>>) {
95+
const [emailMessage, env, context] = args;
96+
return withIsolationScope(isolationScope => {
97+
const options = getFinalOptions(optionsCallback(env), env);
98+
99+
const client = init(options);
100+
isolationScope.setClient(client);
101+
102+
addCloudResourceContext(isolationScope);
103+
104+
return startSpan(
105+
{
106+
op: 'faas.email',
107+
name: `Handle Email ${emailMessage.to}`,
108+
attributes: {
109+
'faas.trigger': 'email',
110+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.email',
111+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
112+
},
113+
},
114+
async () => {
115+
try {
116+
return await (target.apply(thisArg, args) as ReturnType<typeof target>);
117+
} catch (e) {
118+
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
119+
throw e;
120+
} finally {
121+
context.waitUntil(flush(2000));
122+
}
123+
},
124+
);
125+
});
126+
},
127+
});
128+
129+
markAsInstrumented(handler.email);
130+
}
131+
132+
if ('queue' in handler && typeof handler.queue === 'function' && !isInstrumented(handler.queue)) {
133+
handler.queue = new Proxy(handler.queue, {
134+
apply(target, thisArg, args: Parameters<ExportedHandlerQueueHandler<Env, QueueHandlerMessage>>) {
135+
const [batch, env, context] = args;
136+
137+
return withIsolationScope(isolationScope => {
138+
const options = getFinalOptions(optionsCallback(env), env);
139+
140+
const client = init(options);
141+
isolationScope.setClient(client);
142+
143+
addCloudResourceContext(isolationScope);
144+
145+
return startSpan(
146+
{
147+
op: 'faas.queue',
148+
name: `process ${batch.queue}`,
149+
attributes: {
150+
'faas.trigger': 'pubsub',
151+
'messaging.destination.name': batch.queue,
152+
'messaging.system': 'cloudflare',
153+
'messaging.batch.message_count': batch.messages.length,
154+
'messaging.message.retry.count': batch.messages.reduce((acc, message) => acc + message.attempts, 0),
155+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'queue.process',
156+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.queue',
157+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
158+
},
159+
},
160+
async () => {
161+
try {
162+
return await (target.apply(thisArg, args) as ReturnType<typeof target>);
163+
} catch (e) {
164+
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
165+
throw e;
166+
} finally {
167+
context.waitUntil(flush(2000));
168+
}
169+
},
170+
);
171+
});
172+
},
173+
});
174+
175+
markAsInstrumented(handler.queue);
176+
}
177+
178+
if ('tail' in handler && typeof handler.tail === 'function' && !isInstrumented(handler.tail)) {
179+
handler.tail = new Proxy(handler.tail, {
180+
apply(target, thisArg, args: Parameters<ExportedHandlerTailHandler<Env>>) {
181+
const [, env, context] = args;
182+
183+
return withIsolationScope(async isolationScope => {
184+
const options = getFinalOptions(optionsCallback(env), env);
185+
186+
const client = init(options);
187+
isolationScope.setClient(client);
188+
189+
addCloudResourceContext(isolationScope);
190+
191+
try {
192+
return await (target.apply(thisArg, args) as ReturnType<typeof target>);
193+
} catch (e) {
194+
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
195+
throw e;
196+
} finally {
197+
context.waitUntil(flush(2000));
198+
}
199+
});
200+
},
201+
});
202+
203+
markAsInstrumented(handler.tail);
204+
}
205+
90206
// This is here because Miniflare sometimes cannot get instrumented
91-
//
92207
} catch (e) {
93208
// Do not console anything here, we don't want to spam the console with errors
94209
}

0 commit comments

Comments
 (0)