Skip to content
Open
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 .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '521 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
31 changes: 29 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ Work in this release was contributed by @faizansaiyed123, @itisrmk, @kunal649, @

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

- **ref(deno, bun): Use a shared request handler wrapper for `Deno.serve` and `Bun.serve`**
- **ref(deno, bun, cloudflare): Use a shared request handler wrapper for `Deno.serve`, `Bun.serve` and Cloudflare**

`Deno.serve` and `Bun.serve` now use the same request handler wrapper, which Cloudflare will also use. On both runtimes, errors in `OPTIONS` and `HEAD` requests now carry the request data, and the SDK keeps every `Set-Cookie` header of a response instead of only the last one.
`Deno.serve`, `Bun.serve` and Cloudflare's `wrapRequestHandler` now use the same request handler wrapper. On all three, the SDK keeps every `Set-Cookie` header of a response instead of only the last one. On Deno and Bun, errors in `OPTIONS` and `HEAD` requests now carry the request data.

For Deno users:

Expand All @@ -52,6 +52,14 @@ Work in this release was contributed by @faizansaiyed123, @itisrmk, @kunal649, @
- A request to `/` gets the `route` source and the name `GET /`.
- Spans also get `user_agent.original` and `http.request.body.size`.

For Cloudflare users:

- Request spans set `client.address` from `CF-Connecting-IP` (or another forwarding header), as on the other runtimes. This follows `dataCollection.userInfo`, which is `true` by default.
- Request spans get the response header attributes, and events get the `response` context.
- An error from the source of a streamed response is captured (unless `captureErrors` is `false`), and the span gets an error status.
- If you removed `httpServerIntegration`, request bodies follow `dataCollection.httpBodies`. Before, the SDK attached no body.
- Without `AsyncLocalStorage` (for example on Shopify Oxygen), events now also get the request URL and method.

## 11.9.0

### Important Changes
Expand Down Expand Up @@ -259,6 +267,25 @@ Work in this release was contributed by @4ktLuffy, @adityareddy-dev, @dasilvalui

Like the other bundler plugins, `sentryBunPlugin` now sends build telemetry and injects the release it detects (for example, from git). To turn off telemetry, set `telemetry: false`.

- **ref(bun): Use the shared request handler wrapper for `Bun.serve`**

`Bun.serve` now uses the same request handler wrapper as `Deno.serve`. Bun spans now follow the Sentry span conventions for `http.server` spans, so some attributes change:

- The matched route is in `http.route` instead of `url.template`. Routes without parameters now also set `http.route`, with the `route` source.
- The host is in `server.address` instead of `url.domain`.
- A request to `/` is named after the route (for example `GET /`), as on the other runtimes.
- Spans now also have `user_agent.original` and `http.request.body.size`.

If you query Bun spans by `url.template` or `url.domain`, use `http.route` or `server.address` instead. Errors in `OPTIONS` and `HEAD` requests now carry the request data. On all three runtimes, the span now lists every `Set-Cookie` header of a response, not only the last one.

- **ref(cloudflare): Use the shared request handler wrapper**

Cloudflare Workers now use the same request handler wrapper as `Deno.serve` and `Bun.serve`. Changes for Cloudflare users:

- Request spans now set `client.address` from the `CF-Connecting-IP` header (or another forwarding header), as on the other runtimes. This follows `dataCollection.userInfo`, which is `true` by default. To leave it out, set `dataCollection: { userInfo: false }`.
- Request spans now have the response header attributes, and events have the `response` context with the status code.
- When the source of a streamed response fails, the SDK now captures the error and marks the span as failed. With `captureErrors: false`, it does not capture it.

## 11.8.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core';
import {
CLIENT_ADDRESS,
SENTRY_SEGMENT_NAME_SOURCE,
SENTRY_SDK_NAME,
SENTRY_SDK_VERSION,
Expand Down Expand Up @@ -205,6 +206,15 @@ it('sends a streamed span envelope with correct spans for a manually started spa
type: 'string',
value: 'localhost',
},
// From the `CF-Connecting-IP` header that `wrangler dev` sets.
[CLIENT_ADDRESS]: {
type: 'string',
value: '127.0.0.1',
},
Comment thread
cursor[bot] marked this conversation as resolved.
'http.response.header.content-type': {
type: 'array',
value: ['text/plain;charset=UTF-8'],
},
'url.full': {
type: 'string',
value: expect.stringMatching(/^http:\/\/localhost:.+$/),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import type { Event } from '@sentry/core';
import { HTTP_SERVER } from '@sentry/conventions/op';
import { expect, it } from 'vitest';
import { createRunner } from '../../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../../spanUtils';

it('does not capture spans emitted through @opentelemetry/api inside wrapRequestHandler from the /request subpath', async ({
signal,
}) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const event = envelope[1]?.[0]?.[1] as Event;
expect(event.transaction).toBe('GET /');
expect(event.contexts?.trace?.op).toBe('http.server');
expect(event.spans).toEqual([]);
// Only the request's segment span. The OpenTelemetry span is not captured.
const spans = getSpansFromEnvelope(envelope);
expect(spans).toHaveLength(1);
expect(spans[0]?.is_segment).toBe(true);
expect(spans[0]?.name).toBe('GET /');
expect(getSpanOp(spans[0]!)).toBe(HTTP_SERVER);
})
.start(signal);

Expand Down
2 changes: 1 addition & 1 deletion packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Client, Integration, IntegrationFn, MaxRequestBodySize, Span, Span
import { debug, defineIntegration, getClient, withIsolationScope } from '@sentry/core';
import type { AssertTrue, CheckServerRequestOptions, ServerRequestOptions } from '@sentry/core/server';
import { URL_PATH_PARAMETER_KEY_BASE } from '@sentry/conventions/attributes';
import { wrapServerRequestHandler } from '@sentry/server-utils';
import { wrapServerRequestHandler } from '@sentry/server-utils/request-handler';
import type { Server, ServeOptions } from 'bun';

const INTEGRATION_NAME = 'BunServer' as const;
Expand Down
160 changes: 38 additions & 122 deletions packages/cloudflare/src/integrations/httpServer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import type { Client, IntegrationFn, MaxRequestBodySize, Span } from '@sentry/core';
import {
captureBodyFromWinterCGRequest,
defineIntegration,
getIsolationScope,
hasSpansEnabled,
parseStringToURLObject,
safeCallback,
} from '@sentry/core';
import type { AssertTrue, CheckServerRequestOptions } from '@sentry/core/server';
import { isStaticAssetRequest } from '@sentry/core/server';
import { DEBUG_BUILD } from '../debug-build';
import type { Client, Integration, IntegrationFn, MaxRequestBodySize, Span } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import type { AssertTrue, CheckServerRequestOptions, ServerRequestOptions } from '@sentry/core/server';

const INTEGRATION_NAME = 'HttpServer' as const;

Expand Down Expand Up @@ -100,21 +91,45 @@ type _ServerRequestOptionsCheck = AssertTrue<
>
>;

interface HttpServerIntegrationInstance extends HttpServerIntegrationOptions {
name: string;
maxRequestBodySize: MaxRequestBodySize | undefined;
type SupportedServerRequestOptions = Omit<
ServerRequestOptions<Request, Response>,
'sessions' | 'sessionFlushingDelayMS'
>;

// Every supported shared option is a required key, so a new one fails to compile below until the
// integration passes it on.
type HttpServerRequestOptions = {
[K in keyof Required<SupportedServerRequestOptions>]: SupportedServerRequestOptions[K];
};

interface HttpServerIntegration extends Integration {
requestOptions: HttpServerRequestOptions;
}

/**
* The incoming request options of the client's `httpServerIntegration`. Without the integration,
* the SDK attaches no request body, so users can turn body capture off by removing it.
*/
export function getHttpServerRequestOptions(client: Client): ServerRequestOptions<Request, Response> {
return (
client.getIntegrationByName<HttpServerIntegration>(INTEGRATION_NAME)?.requestOptions ?? {
maxRequestBodySize: 'none',
}
);
Comment thread
cursor[bot] marked this conversation as resolved.
}

const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}): HttpServerIntegrationInstance => {
const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}): HttpServerIntegration => {
return {
name: INTEGRATION_NAME,
maxRequestBodySize: options.maxRequestBodySize,
ignoreRequestBody: options.ignoreRequestBody,
ignoreIncomingRequests: options.ignoreIncomingRequests,
ignoreStaticAssets: options.ignoreStaticAssets ?? true,
spans: options.spans,
onSpanCreated: options.onSpanCreated,
onSpanEnd: options.onSpanEnd,
requestOptions: {
maxRequestBodySize: options.maxRequestBodySize,
ignoreRequestBody: options.ignoreRequestBody,
ignoreIncomingRequests: options.ignoreIncomingRequests,
ignoreStaticAssets: options.ignoreStaticAssets ?? true,
spans: options.spans,
onSpanCreated: options.onSpanCreated,
onSpanEnd: options.onSpanEnd,
},
};
}) satisfies IntegrationFn;

Expand All @@ -137,102 +152,3 @@ const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}): Ht
* ```
*/
export const httpServerIntegration = defineIntegration(_httpServerIntegration);

/**
* Capture the request body based on the HttpServer integration config.
* Called internally by `wrapRequestHandler`.
*/
export async function captureIncomingRequestBody(client: Client, request: Request): Promise<void> {
const integration = client.getIntegrationByName<HttpServerIntegrationInstance>(INTEGRATION_NAME);

if (!integration) {
return;
}

const configuredBodySize = integration.maxRequestBodySize;
const effectiveBodySize: MaxRequestBodySize =
configuredBodySize ??
(client.getDataCollectionOptions().httpBodies.includes('incomingRequest') ? 'medium' : 'none');

if (effectiveBodySize === 'none') {
return;
}

// Skip GET and HEAD requests - they don't have bodies
// Also skip OPTIONS, even if they may have a body, they might not give a lot of extra value
if (request.method === 'GET' || request.method === 'HEAD' || request.method === 'OPTIONS') {
return;
}

const ignoreRequestBody = integration.ignoreRequestBody;
if (
ignoreRequestBody &&
// If the filter throws, skip the body so it cannot leak data the filter was meant to hide.
safeCallback(
DEBUG_BUILD ? 'Error in `ignoreRequestBody`, skipping the body:' : '',
() => ignoreRequestBody(request.url, request),
() => true,
)
) {
return;
}

const isolationScope = getIsolationScope();
await captureBodyFromWinterCGRequest(request, isolationScope, effectiveBodySize);
}

/**
* Whether to create a span for the incoming request, based on the HttpServer integration config.
* Called internally by `wrapRequestHandler`.
*/
export function shouldCreateIncomingRequestSpan(client: Client | undefined, request: Request): boolean {
if (!client) {
return false;
}

const integration = client.getIntegrationByName<HttpServerIntegrationInstance>(INTEGRATION_NAME);
if (!(integration?.spans ?? hasSpansEnabled(client.getOptions()))) {
return false;
}

const urlObject = parseStringToURLObject(request.url);
const urlPath = urlObject ? `${urlObject.pathname}${urlObject.search}` : '/';
if (integration?.ignoreStaticAssets && request.method === 'GET' && isStaticAssetRequest(urlPath)) {
return false;
}

const ignoreIncomingRequests = integration?.ignoreIncomingRequests;
return !(
ignoreIncomingRequests &&
safeCallback(
DEBUG_BUILD ? 'Error in `ignoreIncomingRequests`, creating the span:' : '',
() => ignoreIncomingRequests(urlPath, request),
() => false,
)
);
}

/**
* The `onSpanCreated` and `onSpanEnd` hooks of the HttpServer integration, guarded so that a hook
* that throws does not fail the request. Called internally by `wrapRequestHandler`.
*/
export function getIncomingRequestSpanHooks(client: Client | undefined): {
onSpanCreated: (span: Span, request: Request) => void;
onSpanEnd: (span: Span, request: Request, response: Response) => void;
} {
const integration = client?.getIntegrationByName<HttpServerIntegrationInstance>(INTEGRATION_NAME);
return {
onSpanCreated: (span, request) =>
safeCallback(
DEBUG_BUILD ? 'Error in `onSpanCreated`:' : '',
() => integration?.onSpanCreated?.(span, request),
() => undefined,
),
onSpanEnd: (span, request, response) =>
safeCallback(
DEBUG_BUILD ? 'Error in `onSpanEnd`:' : '',
() => integration?.onSpanEnd?.(span, request, response),
() => undefined,
),
};
}
8 changes: 0 additions & 8 deletions packages/cloudflare/src/scope-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { Scope } from '@sentry/core';
import { winterCGRequestToRequestData } from '@sentry/core';

/**
* Set cloud resource context on scope.
Expand All @@ -19,10 +18,3 @@ export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties
timezone: cf.timezone,
});
}

/**
* Set request data on scope
*/
export function addRequest(scope: Scope, request: Request): void {
scope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(request) });
}
Loading
Loading