Skip to content

feat(core): Instrument workers-ai-provider#22119

Open
JPeer264 wants to merge 8 commits into
developfrom
jp/cloudflare-workers-ai
Open

feat(core): Instrument workers-ai-provider#22119
JPeer264 wants to merge 8 commits into
developfrom
jp/cloudflare-workers-ai

Conversation

@JPeer264

@JPeer264 JPeer264 commented Jul 8, 2026

Copy link
Copy Markdown
Member

closes #20839
closes #20839

This is instrumenting workers-ai, but it needs to be wrapped, as in Cloudflare there is no monkey patching for bindings. In a follow-up PR this will be included in the instrumentEnv within Cloudflare.

Tbh I would have added this into packages/cloudflare, but all AI integrations do live in core, and there are also some utils which are only there and would have needed to export, such as endStreamSpan and all the gen-ai-attributes. The downside of this is that the workers-ai types needed to be vendored in (in the packages/cloudflare we could have taken them from the @cloudflare/workers-types package.

@JPeer264 JPeer264 self-assigned this Jul 8, 2026
@linear-code

linear-code Bot commented Jul 8, 2026

Copy link
Copy Markdown

JS-2449

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 27.74 kB - -
@sentry/browser - with treeshaking flags 26.18 kB - -
@sentry/browser (incl. Tracing) 46.55 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.35 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.36 kB - -
@sentry/browser (incl. Tracing, Replay) 85.81 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 75.46 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 90.52 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 103.2 kB - -
@sentry/browser (incl. Feedback) 44.93 kB - -
@sentry/browser (incl. sendFeedback) 32.54 kB - -
@sentry/browser (incl. FeedbackAsync) 37.68 kB - -
@sentry/browser (incl. Metrics) 28.83 kB - -
@sentry/browser (incl. Logs) 29.06 kB - -
@sentry/browser (incl. Metrics & Logs) 29.75 kB - -
@sentry/react 29.54 kB - -
@sentry/react (incl. Tracing) 48.81 kB - -
@sentry/vue 33.17 kB - -
@sentry/vue (incl. Tracing) 48.52 kB - -
@sentry/svelte 27.77 kB - -
CDN Bundle 30.14 kB - -
CDN Bundle (incl. Tracing) 48.52 kB - -
CDN Bundle (incl. Logs, Metrics) 31.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.82 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.97 kB - -
CDN Bundle (incl. Tracing, Replay) 86.03 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 87.34 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 91.83 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 93.09 kB - -
CDN Bundle - uncompressed 89.87 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.68 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.58 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 150.66 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 219.32 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 265.89 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 269.86 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 279.6 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 283.55 kB - -
@sentry/nextjs (client) 51.36 kB - -
@sentry/sveltekit (client) 46.98 kB - -
@sentry/core/server 79.87 kB +1.5% +1.18 kB 🔺
@sentry/core/browser 66.22 kB +1.8% +1.17 kB 🔺
@sentry/node-core 63.21 kB - -
@sentry/node 125.52 kB +0.01% +1 B 🔺
@sentry/node (incl. diagnostics channel injection) 148.06 kB - -
@sentry/node/import (ESM hook with diagnostics-channel injection) 70.03 kB - -
@sentry/node/light 51.32 kB - -
@sentry/node - without tracing 74.71 kB - -
@sentry/aws-serverless 83.92 kB - -
@sentry/cloudflare (withSentry) - minified 182.57 kB - -
@sentry/cloudflare (withSentry) 452.25 kB - -

View base workflow run

@JPeer264

JPeer264 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a22515d. Configure here.

Comment thread packages/core/src/tracing/workers-ai/index.ts
Comment thread packages/core/src/tracing/workers-ai/index.ts
@JPeer264
JPeer264 force-pushed the jp/cloudflare-workers-ai branch from 0aa52a5 to 8011b0b Compare July 13, 2026 15:10
@JPeer264
JPeer264 marked this pull request as ready for review July 13, 2026 15:16
@JPeer264
JPeer264 requested review from a team, andreiborza, isaacs, mydea and nicohrubec and removed request for a team July 13, 2026 15:16
Comment thread packages/core/src/tracing/workers-ai/index.ts Outdated
Comment thread packages/core/src/tracing/workers-ai/constants.ts Outdated
Comment thread packages/core/src/tracing/workers-ai/index.ts Outdated

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor comments/questions. Overall a worthwhile and well-factored addition that follows the patterns established already. 👍

Comment thread packages/core/src/tracing/workers-ai/index.ts Outdated
Comment thread packages/core/src/tracing/workers-ai/index.ts
},
error => {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
captureException(error, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low/question: I thought the prevailing wisdom was that we don't call captureException, except in cases where we know that the user has not caught the error, and prefer instead to only do that at the top level. Is this different in cf workers? It seems like someone could try { await ai.run(...) } catch { /* this is fine */ }, and end up with exceptions in their dashboard, no?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good question indeed. I actually tried to replicate the original integrations as much as possible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only do this if there is no way to capture these exceptions otherwise. E.g. in vercel ai v4, tool errors bubble up so we do not capture them here. In v5+ we capture them in the integration because they otherwise do not bubble up, as an example.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually tried to replicate the original integrations as much as possible.

Makes sense. There's definitely a good argument leave to as-is and addressed in a follow-up (or we can decide not to).

In v5+ we capture them in the integration because they otherwise do not bubble up

@mydea What happens to these errors in v5?

If they don't bubble up, are they really "exceptions"? That is, isn't the purpose of captureException to be sort of "this is why your program crashed"?

@nicohrubec nicohrubec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two questions:

  1. could you maybe add a screenshot showing a trace in sentry with this instrumentation?
  2. the main reason our AI instrumentations still live in core is that we currently still export these to the browser (which will change in v11, we'll then move all AI integrations to server-utils). given that this is specific to cloudflare, we could think about moving this to the cloudflare package as you mentioned or if not that maybe move it to server utils?

return result;
},
error => {
captureException(error, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: do we want to immediately capture the exception here? I think we only want to do that if we are fairly certain that the user basically can't handle this exception manually not sure if that is the case here. I know this is what we do in other instrumentations currently, but we might have to revisit this there too eventually

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it is good if we start to be inconsistent here. I'd vote for removing the captureException here, but I'm not sure if it is great if one out of 7 is different to the others

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also follow up on this, since we have the same pattern in all other integrations probably something to look into holistically. Could you please create a follow-up ticket for this so we don't forget?

@JPeer264

Copy link
Copy Markdown
Member Author

@nicohrubec

could you maybe add a screenshot showing a trace in sentry with this instrumentation?

#bakingassistant

https://sentry-sdks.sentry.io/explore/traces/trace/7b5d5e7a7c3542edba07821a7025c804/

Screenshot 2026-07-17 at 10 48 22

the main reason our AI instrumentations still live in core is that we currently still export these to the browser (which will change in v11, we'll then move all AI integrations to server-utils). given that this is specific to cloudflare, we could think about moving this to the cloudflare package as you mentioned or if not that maybe move it to server utils?

I like the idea of having it in Cloudflare only, as there I could also use the types directly. I don't think these bindings would ever be available outside of Cloudflare, and if it would be - then we can still move it to server-utils. Do you suggest to already do this in this PR or in a follow up once we start with the v11 alpha cycle?

Comment on lines +162 to +164
if (Array.isArray(response.tool_calls) && response.tool_calls.length > 0) {
span.setAttribute(GEN_AI_OUTPUT_MESSAGES, JSON.stringify(response.tool_calls));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The addResponseAttributes function overwrites the response text attribute with tool call data when both are present in an AI response, causing data loss in traces.
Severity: MEDIUM

Suggested Fix

Instead of calling span.setAttribute twice, combine the response text and tool_calls into a single array of parts, similar to how the Vercel AI integration handles it. Then, set the GEN_AI_OUTPUT_MESSAGES attribute just once with the combined, stringified data.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/core/src/tracing/workers-ai/utils.ts#L162-L164

Potential issue: In the `addResponseAttributes` function, when a Workers AI response
contains both a text `response` and a `tool_calls` array, the span attribute
`GEN_AI_OUTPUT_MESSAGES` is set twice. The first call sets the text response, but the
second call immediately overwrites it with the `tool_calls` data. This results in the
silent loss of the text response from the span attributes, leading to incomplete tracing
data for tool-use scenarios that also include explanatory text.

@JPeer264
JPeer264 force-pushed the jp/cloudflare-workers-ai branch from b7852b2 to 90177eb Compare July 17, 2026 10:00
@JPeer264
JPeer264 force-pushed the jp/cloudflare-workers-ai branch from 1b95295 to 16771a7 Compare July 17, 2026 10:28
}

return startSpan(spanConfig, (span: Span) => {
const originalResult = originalRun.apply(context, args) as Promise<unknown>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Synchronous errors in the non-streaming run method are not reported to Sentry as exceptions, only as a span status.
Severity: LOW

Suggested Fix

Wrap the originalRun.apply(context, args) call in a try...catch block, similar to the streaming path. In the catch block, call captureException to report the error to Sentry before re-throwing it.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/core/src/tracing/workers-ai/index.ts#L90

Potential issue: In the non-streaming path for Workers AI tracing, if the original `run`
method throws a synchronous error, the error is not reported to Sentry via
`captureException`. The error is caught by `startSpan`'s internal `handleCallbackErrors`
function, which sets the span status to error but does not create a Sentry exception
event. This is inconsistent with the streaming path, which explicitly catches
synchronous errors and reports them. While synchronous throws from async methods are
unlikely, this represents a gap in error monitoring.

}

if (Array.isArray(response.tool_calls) && response.tool_calls.length > 0) {
span.setAttribute(GEN_AI_OUTPUT_MESSAGES, JSON.stringify(response.tool_calls));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: JSON.stringify in addResponseAttributes can throw an unhandled exception on non-serializable data, which is not reported to Sentry.
Severity: MEDIUM

Suggested Fix

Wrap the JSON.stringify calls within addResponseAttributes in a try...catch block, similar to the getTruncatedJsonString utility used in other integrations. On error, a placeholder like '[unserializable]' should be used for the attribute value. Alternatively, wrap the call to addResponseAttributes in the non-streaming path with error handling that calls captureException.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/core/src/tracing/workers-ai/utils.ts#L164

Potential issue: In the non-streaming path for Workers AI tracing,
`addResponseAttributes` is called on the successful promise resolution. This function
calls `JSON.stringify` on the AI model's response without a `try...catch` block. If the
response contains non-serializable data (e.g., circular references), `JSON.stringify`
will throw an exception. This exception is not caught and reported to Sentry via
`captureException`, leading to a silent monitoring failure where the error goes
undetected. The streaming path correctly handles this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudflare instrument workers-ai

5 participants