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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ Work in this release was contributed by @adityareddy-dev, @jpdefo, @Jxxunnn, @ph
- The SDK no longer reads a streamed response ahead of the client.
- Errors in `OPTIONS` and `HEAD` requests now carry the request data.

- **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
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
@@ -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',
}
);
}

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