Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursor/BUGBOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Keep reviews high-signal. Prefer actionable, high-confidence findings over specu
- What to do instead: Use an existing reference to the scope or client. For example, this is possible in most `Integration` hooks.
- Flag hard-coded attribute names and span ops (e.g. `'sentry.origin'`, `'sentry.op'`, `'http.request.method'`, `'db.system.name'`) when `@sentry/conventions` exports a constant for them. Use the constant from `@sentry/conventions/attributes` (e.g. `SENTRY_ORIGIN`, `SENTRY_OP`) or `@sentry/conventions/op` instead. Do not flag values that have no constant in `@sentry/conventions`.
- Flag unnecessary `span.setAttribute(s)` calls: If data is already available at span start, it must be set via the `attributes` option of `startSpan`, `startSpanManual`, `startInactiveSpan` or `startIdleSpan` calls. This ensures that as much context as possible is available when `tracesSampler` or `ignoreSpans` SDK options are applied. If a `span.setAttribute(s)` call happens at a later time than right after span start and the attribute value can only be computed at that time, do not flag it.
- Flag a new option on an integration for incoming HTTP requests that is not in `ServerRequestOptions` (`packages/core/src/integrations/http/server-request-options.ts`), when it applies to more than one runtime. These integrations are Node `httpIntegration`, Deno `denoHttpIntegration` and `denoServeIntegration`, Bun `bunHttpServerIntegration` and `bunServerIntegration`, and Cloudflare `httpServerIntegration`. Add the option to `ServerRequestOptions`, then support it in each runtime or list it as unsupported in that runtime's `CheckServerRequestOptions` check. Also flag a runtime option that matches a shared option in purpose but has a different name or meaning.

### Code quality

Expand Down
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,53 @@ Work in this release was contributed by @adityareddy-dev, @jpdefo, @Jxxunnn, @ph

`denoServeIntegration` now records a request session for each incoming request, as `denoHttpIntegration` does for `node:http`. This gives release health and crash-free rates for `Deno.serve` apps. Sessions are sent only when a `release` is set. To turn them off, set `denoServeIntegration({ sessions: false })`. `sessionFlushingDelayMS` sets how long the SDK waits before it sends the session aggregate.

- **feat(bun): Add incoming request options and request sessions to `bunServerIntegration`**

`bunServerIntegration` now accepts the same options as `denoServeIntegration`: `ignoreRequestBody`, `ignoreIncomingRequests`, `ignoreStaticAssets`, `spans`, `onSpanCreated`, `onSpanEnd`, `sessions` and `sessionFlushingDelayMS`. The callbacks receive the web `Request`.

`Bun.serve` now records a request session for each incoming request. Sessions are sent only when a `release` is set. To turn them off, set `bunServerIntegration({ sessions: false })`.

`ignoreStaticAssets` defaults to `true`, so `Bun.serve` no longer creates spans for `GET` requests to common static assets such as `favicon.ico`, `robots.txt` or `*.js` files. To keep these spans, set `bunServerIntegration({ ignoreStaticAssets: false })`. `Bun.serve` spans also follow the client's tracing configuration by default, so the integration does not start spans when tracing is disabled. Errors thrown for `OPTIONS` and `HEAD` requests are now captured.

- **feat(node): Add `onSpanEnd` to `httpIntegration`**

`httpIntegration` and `httpServerSpansIntegration` now accept `onSpanEnd`, a hook that runs when the response for an incoming request finishes, before its span ends. Use it to change the span based on the response. Deno and Bun already have this option.

- **feat(cloudflare): Add incoming request span options to `httpServerIntegration`**

Cloudflare's `httpServerIntegration` now accepts `ignoreIncomingRequests`, `ignoreStaticAssets`, `spans`, `onSpanCreated` and `onSpanEnd`, with the same meaning as in the Node, Deno and Bun integrations. The callbacks receive the web `Request`.

`ignoreStaticAssets` defaults to `true`, so workers no longer create spans for `GET` requests to common static assets such as `favicon.ico`, `robots.txt` or `*.js` files. To keep these spans, set `httpServerIntegration({ ignoreStaticAssets: false })`. Request spans also follow the client's tracing configuration by default, so the SDK does not start them when tracing is disabled. If `ignoreRequestBody` throws, the request no longer fails, and the SDK skips the body.

- **feat(node, bun, deno, cloudflare): Align portable exports across server SDKs**

Node, Bun, Deno and Cloudflare now export the same helpers from `@sentry/core` and `@sentry/server-utils` where they work on that runtime:

- Node and Bun now export `moduleMetadataIntegration`, `instrumentPostgresJsSql`, `instrumentCreateReactAgent`, `awsIntegration` and the `FlueOptions` type. `awsIntegration` was already in their default integrations.
- Deno and Cloudflare now export `parameterize` and `instrumentStateGraphCompile`.
- Bun and Cloudflare now export the `Log` and `LogSeverityLevel` types.
- Deno now exports `fastifyIntegration`, which was already in its default integrations.

- **feat(cloudflare): Export the channel integrations**

When the `@sentry/cloudflare/vite` plugin bundles an instrumented package (for example `pg` or `openai`), the SDK adds the matching integration with its default options. `@sentry/cloudflare` now exports these integrations, so you can set their options, for example:

```ts
Sentry.withSentry(
env => ({
dsn: env.SENTRY_DSN,
integrations: [Sentry.openAIIntegration({ recordInputs: false, recordOutputs: false })],
}),
handler,
);
```

These integrations need the vite plugin. Without it, they record nothing, and with `debug: true` the SDK logs a warning.

- **fix(cloudflare): Do not add back channel integrations the user removed**

When an instrumented package loaded after `init()` (for example through a dynamic `import()`), the SDK added its integration even if the user had removed it. This applied to an `integrations` function that filtered it out, `defaultIntegrations: false`, and a custom `defaultIntegrations` list. The SDK now applies the same rules to a late-loaded integration as to the defaults at `init()`. To do this, it calls an `integrations` function again, once for each package that loads after `init()`.

## 11.8.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as Sentry from '@sentry/cloudflare';

interface Env {
SENTRY_DSN: string;
}

// A plain worker. The `@sentry/cloudflare/vite` plugin wraps it with `withSentry` at build time,
// with the options from `instrument.server.ts`.
export default {
async fetch(request: Request): Promise<Response> {
const { pathname } = new URL(request.url);

if (pathname === '/health') {
Sentry.captureMessage('Health check');
return new Response('ok');
}

if (pathname === '/favicon.ico') {
Sentry.captureMessage('Static asset');
return new Response('icon');
}

if (pathname === '/health/stream') {
// No span for this request. The message is captured after the handler returns, while the
// body streams, so it is only sent if the SDK waits for the stream to finish.
const encoder = new TextEncoder();
const body = new ReadableStream<Uint8Array>({
async start(controller) {
controller.enqueue(encoder.encode('a'));
await new Promise(resolve => setTimeout(resolve, 50));
Sentry.captureMessage('Streamed after handler return');
controller.enqueue(encoder.encode('b'));
controller.close();
},
});
return new Response(body, { headers: { 'content-type': 'text/event-stream' } });
}

return new Response('ok', { status: 201 });
},
} satisfies ExportedHandler<Env>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineCloudflareOptions, httpServerIntegration } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
traceLifecycle: 'static',
integrations: [
httpServerIntegration({
ignoreIncomingRequests: urlPath => urlPath.startsWith('/health'),
onSpanCreated: (span, request) => {
span.setAttribute('test.on_span_created', new URL(request.url).pathname);
},
onSpanEnd: (span, _request, response) => {
span.setAttribute('test.on_span_end', response.status);
},
}),
],
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Envelope, Event } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';

function itemOf(envelope: Envelope): { type: string; event: Event } {
const [header, event] = envelope[1][0] as [{ type: string }, Event];
return { type: header.type, event };
}

function messageEvent(message: string): (envelope: Envelope) => void {
return envelope => {
const { type, event } = itemOf(envelope);
expect(type).toBe('event');
expect(event.message).toBe(message);
};
}

// The `onSpanCreated` and `onSpanEnd` hooks of the worker set these attributes.
function apiTransaction(envelope: Envelope): void {
const { type, event } = itemOf(envelope);
expect(type).toBe('transaction');
expect(event.transaction).toBe('GET /api');
expect(event.contexts?.trace?.data).toEqual(
expect.objectContaining({
'test.on_span_created': '/api',
'test.on_span_end': 201,
}),
);
}

// Each test makes a request without a span, then a request with one. With `failOnUnexpected`, a
// transaction for the first request fails the test, as it arrives before the second one.

it('does not create a span for a request that ignoreIncomingRequests matches', async ({ signal }) => {
const runner = createRunner(__dirname)
.unordered()
.failOnUnexpected()
.expect(messageEvent('Health check'))
.expect(apiTransaction)
.start(signal);

await runner.makeRequest('get', '/health');
await runner.makeRequest('get', '/api');
await runner.completed();
});

it('does not create a span for a static asset request by default', async ({ signal }) => {
const runner = createRunner(__dirname)
.unordered()
.failOnUnexpected()
.expect(messageEvent('Static asset'))
.expect(apiTransaction)
.start(signal);

await runner.makeRequest('get', '/favicon.ico');
await runner.makeRequest('get', '/api');
await runner.completed();
});

it('sends events from a streamed response without a span after the stream finishes', async ({ signal }) => {
const runner = createRunner(__dirname)
.unordered()
.failOnUnexpected()
.expect(messageEvent('Streamed after handler return'))
.expect(apiTransaction)
.start(signal);

expect(await runner.makeRequest<string>('get', '/health/stream')).toBe('ab');
await runner.makeRequest('get', '/api');
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cloudflare } from '@cloudflare/vite-plugin';
import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [cloudflare(), sentryCloudflareVitePlugin()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "../../../node_modules/wrangler/config-schema.json",
"name": "cloudflare-integrations-http-server-spans",
// `main` points at the source entry. The Sentry Vite plugin builds from it, and the runner serves
// the built output.
"main": "index.ts",
"compatibility_date": "2025-06-17",
"compatibility_flags": ["nodejs_compat"],
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ it('should add server.address attribute to metrics when serverName is set', asyn
name: 'test.counter',
type: 'counter',
value: 1,
span_id: expect.any(String),
timestamp: expect.any(Number),
trace_id: expect.any(String),
attributes: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/cloudflare';

interface Env {
SENTRY_DSN: string;
}

// A plain worker. The `@sentry/cloudflare/vite` plugin wraps it with `withSentry` at build time,
// with the options from `instrument.server.ts`.
export default {
async fetch(): Promise<Response> {
const userId = 42;
Sentry.captureMessage(Sentry.parameterize`User ${userId} logged in`);
return new Response('ok');
},
} satisfies ExportedHandler<Env>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Event } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';

it('parameterize from the public entry sends a message template and its values', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const event = envelope[1]?.[0]?.[1] as Event;
expect(event.logentry).toEqual({ message: 'User %s logged in', params: [42] });
})
.start(signal);

await runner.makeRequest('get', '/');
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cloudflare } from '@cloudflare/vite-plugin';
import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [cloudflare(), sentryCloudflareVitePlugin()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "../../../node_modules/wrangler/config-schema.json",
"name": "cloudflare-public-api-parameterize",
// `main` points at the source entry. The Sentry Vite plugin builds from it, and the runner serves
// the built output.
"main": "index.ts",
"compatibility_date": "2025-06-17",
"compatibility_flags": ["nodejs_compat"],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as Sentry from '@sentry/cloudflare';
import OpenAI from 'openai';

interface Env {
SENTRY_DSN: string;
}

// Return a canned response so the `openai` SDK runs on workerd without hitting the network.
const mockFetch: typeof fetch = async () =>
new Response(
JSON.stringify({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: 'gpt-3.5-turbo',
choices: [
{
index: 0,
message: { role: 'assistant', content: 'Hello from OpenAI!' },
finish_reason: 'stop',
},
],
usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 },
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
// `dataCollection` records inputs by default. The integration options must win over it.
integrations: [Sentry.openAIIntegration({ recordInputs: false })],
}),
{
async fetch(_request, _env, _ctx) {
// Not wrapped with `instrumentOpenAiClient`: the vite plugin instruments the `openai` package.
const client = new OpenAI({ apiKey: 'mock-api-key', fetch: mockFetch });
await client.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});
return new Response('ok');
},
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { GEN_AI_INPUT_MESSAGES, GEN_AI_RESPONSE_TEXT, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
import { GEN_AI_CHAT } from '@sentry/conventions/op';
import { expect, it } from 'vitest';
import { createRunner } from '../../../../runner';

it('applies the options of an openAIIntegration the user configured', async ({ signal }) => {
const runner = createRunner(__dirname).ignore('event').start(signal);

// The segment span and the gen_ai child. The segment can arrive before the child's envelope.
const spansPromise = runner.collectStreamedSpans(spansOfTrace => spansOfTrace.length === 2);

await runner.makeRequest('get', '/');

const genAiSpans = (await spansPromise).filter(span => !span.is_segment);
expect(genAiSpans).toHaveLength(1);
const attributes = genAiSpans[0]?.attributes;

// The vite plugin instrumented `openai`, so the channel integration created the span.
expect(attributes?.[SENTRY_OP]).toEqual({ type: 'string', value: GEN_AI_CHAT });
expect(attributes?.[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai' });
// `recordInputs: false` from the configured integration wins over `dataCollection`, which records
// inputs by default. Outputs keep the default.
expect(attributes?.[GEN_AI_INPUT_MESSAGES]).toBeUndefined();
expect(attributes?.[GEN_AI_RESPONSE_TEXT]).toEqual({ type: 'string', value: '["Hello from OpenAI!"]' });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cloudflare } from '@cloudflare/vite-plugin';
import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [cloudflare(), sentryCloudflareVitePlugin({ autoInstrumentation: false })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "openai-options-worker",
"compatibility_date": "2026-04-26",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
25 changes: 25 additions & 0 deletions dev-packages/deno-integration-tests/suites/parameterize/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// <reference lib="deno.ns" />

import * as Sentry from '@sentry/deno';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
import { errorSink, resetGlobals, withTimeout } from '../../src/index.ts';

Deno.test('parameterize from the public entry sends a message template and its values', async () => {
resetGlobals();
const sink = errorSink();

Sentry.init({
dsn: 'https://username@domain/123',
beforeSend: sink.beforeSend,
});

const userId = 42;
Sentry.captureMessage(Sentry.parameterize`User ${userId} logged in`);

const event = await withTimeout(
sink.waitFor(e => e.logentry?.message === 'User %s logged in'),
5_000,
'parameterized message event',
);
assertEquals(event.logentry, { message: 'User %s logged in', params: [42] });
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default withSentryConfig(nextConfig, {
silent: true,
applicationKey: 'nextjs-16-e2e',
reactComponentAnnotation: {
enabled: true,
enabled: false,
},
_experimental: {
vercelCronsMonitoring: true,
Expand Down
Loading
Loading