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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

- **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.
- Request spans now list every `Set-Cookie` header of a response, as on Deno and Bun.
- Without `AsyncLocalStorage` (for example on Shopify Oxygen), events now also get the request URL and method.

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

Work in this release was contributed by @lazerg and @chiliec. Thank you for your contributions!
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
161 changes: 39 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,46 @@ 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`. If the integration does not
* set `maxRequestBodySize`, `dataCollection.httpBodies` decides. Without the integration, the SDK
* attaches no request body.
*/
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 +153,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,7 +1,6 @@
import { CLOUD_PROVIDER } from '@sentry/conventions/attributes';
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 @@ -20,10 +19,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