Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@

Work in this release was contributed by @adityareddy-dev, @jpdefo, @Jxxunnn, @philipatkinson, @Shubham-Padkonde, @mikamikasuki, and @mketiku. Thank you for your contributions!

- **feat(deno): Add incoming request options to `denoServeIntegration`**

`denoServeIntegration` now accepts `ignoreRequestBody`, `ignoreIncomingRequests`, `ignoreStaticAssets`, `spans`, `onSpanCreated` and `onSpanEnd`, with the same meaning as in `denoHttpIntegration`. The callbacks receive the web `Request`.

`ignoreStaticAssets` defaults to `true`, so `Deno.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 `denoServeIntegration({ ignoreStaticAssets: false })`. `Deno.serve` spans also follow the client's tracing configuration by default, so the integration does not start spans when tracing is disabled.

## 11.8.0

### Important Changes
Expand Down
52 changes: 51 additions & 1 deletion packages/deno/src/integrations/deno-serve.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Event, IntegrationFn, MaxRequestBodySize } from '@sentry/core';
import type { Event, IntegrationFn, MaxRequestBodySize, Span } from '@sentry/core';
import { debug, defineIntegration } from '@sentry/core';
import { DEFAULT_IGNORE_STATUS_CODES, processHttpServerTransactionEvent } from '@sentry/core/server';
import type { RequestHandlerWrapperOptions } from '../wrap-deno-request-handler';
Expand All @@ -17,6 +17,50 @@ export type DenoServeIntegrationOptions = {
*/
maxRequestBodySize?: MaxRequestBodySize;

/**
* Do not capture the request body for incoming requests where the given callback returns `true`.
*
* `url` is the full URL of the request, including the query string. `request` is the incoming
* {@link Request}. The callback runs before the SDK reads the body.
*/
ignoreRequestBody?: (url: string, request: Request) => boolean;

/**
* Do not capture server spans for incoming requests whose URL path makes the given callback return `true`.
*
* `urlPath` is the path of the request, including the query string. `request` is the incoming
* {@link Request}. Errors thrown by the handler are still captured.
*/
ignoreIncomingRequests?: (urlPath: string, request: Request) => boolean;

/**
* Whether to automatically ignore common static asset requests (favicon.ico, robots.txt, etc.)
* when creating server spans.
*
* @default `true`
*/
ignoreStaticAssets?: boolean;

/**
* Whether to create spans for incoming requests.
* Errors are still captured and request data is still attached when this is `false`.
* Defaults to the client's tracing configuration (`hasSpansEnabled`).
*/
spans?: boolean;

/**
* A hook that can be used to mutate the span for incoming requests.
* This is triggered after the span is created, before the request handler runs.
*/
onSpanCreated?: (span: Span, request: Request) => void;

/**
* A hook that can be used to mutate the span one last time before it ends.
* For a streamed response, this runs when the stream finishes.
* It is not called when the request handler throws.
*/
onSpanEnd?: (span: Span, request: Request, response: Response) => void;

/**
* Do not capture spans for incoming `Deno.serve` requests with the given status codes.
* By default, some 3xx and 4xx status codes are dropped (see @default).
Expand Down Expand Up @@ -99,6 +143,12 @@ const _denoServeIntegration = ((options: DenoServeIntegrationOptions = {}) => {
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,
processEvent(event: Event): Event | null {
// Gated on this integration's own span origin so it does not filter `node:http`
// transactions, which `denoHttpIntegration` owns via its own `ignoreStatusCodes`.
Expand Down
25 changes: 14 additions & 11 deletions packages/deno/src/utils/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,32 @@ import { classifyResponseStreaming } from '@sentry/core/server';
* Tee a stream, and end the provided span when the stream ends.
* Returns the other side of the tee, which can be used to send the
* response to a client.
*
* `onEnd` runs right before the span ends.
*/
export async function streamResponse(span: Span, res: Response): Promise<Response> {
export async function streamResponse(span: Span, res: Response, onEnd?: () => void): Promise<Response> {
const classification = classifyResponseStreaming(res);
const endSpan = (): void => {
onEnd?.();
span.end();
};
Comment thread
cursor[bot] marked this conversation as resolved.

// not streaming, just end the span and return the response
if (!classification.isStreaming || !res.body) {
span.end();
endSpan();
return res;
}

// Streaming response detected - monitor consumption to keep span alive
try {
return new Response(
monitorStream(res.body, () => span.end()),
{
status: res.status,
statusText: res.statusText,
headers: res.headers,
},
);
return new Response(monitorStream(res.body, endSpan), {
status: res.status,
statusText: res.statusText,
headers: res.headers,
});
} catch {
// tee() failed - handle without streaming
span.end();
endSpan();
return res;
}
}
Expand Down
104 changes: 81 additions & 23 deletions packages/deno/src/wrap-deno-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
SENTRY_SEGMENT_NAME_SOURCE,
} from '@sentry/conventions/attributes';
import { HTTP_SERVER } from '@sentry/conventions/op';
import type { Integration, MaxRequestBodySize } from '@sentry/core';
import type { Client, Integration } from '@sentry/core';
import {
captureBodyFromWinterCGRequest,
captureException,
continueTrace,
debug,
getClient,
getHttpSpanDetailsFromUrlObject,
hasSpansEnabled,
hasSpanStreamingEnabled,
httpHeadersToSpanAttributes,
HTTP_SPAN_NAME_FALLBACK,
Expand All @@ -24,7 +25,8 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
import { getClientIPAddress } from '@sentry/core/server';
import { getClientIPAddress, isStaticAssetRequest } from '@sentry/core/server';
import type { DenoServeIntegrationOptions } from './integrations/deno-serve';
import { streamResponse } from './utils/streaming';

export type RequestHandlerWrapperOptions<Addr extends Deno.Addr> = {
Expand All @@ -33,6 +35,45 @@ export type RequestHandlerWrapperOptions<Addr extends Deno.Addr> = {
serveOptions?: Deno.ServeOptions<Addr>;
};

function captureServeError(e: unknown): void {
captureException(e, {
mechanism: {
handled: false,
type: 'auto.http.deno',
data: { function: 'serve' },
},
});
}

// A user callback that throws must not fail the request or leave the span open.
function callUserCallback<T>(name: string, callback: () => T, fallback: T): T {
try {
return callback();
} catch (e) {
debug.error(`Error in the Deno.serve ${name} callback.`, e);
return fallback;
}
}

function shouldCreateSpan(
client: Client,
integration: DenoServeIntegrationOptions | undefined,
request: Request,
urlPath: string,
): boolean {
if (!(integration?.spans ?? hasSpansEnabled(client.getOptions()))) {
return false;
}
if (integration?.ignoreStaticAssets && request.method === 'GET' && isStaticAssetRequest(urlPath)) {
return false;
}
const ignoreIncomingRequests = integration?.ignoreIncomingRequests;
return !(
ignoreIncomingRequests &&
callUserCallback('ignoreIncomingRequests', () => ignoreIncomingRequests(urlPath, request), false)
);
}

const assignIfSet = <T extends Record<string, unknown>, K extends keyof T>(
obj: T,
key: K,
Expand Down Expand Up @@ -63,17 +104,13 @@ export const wrapDenoRequestHandler = <Addr extends Deno.Addr = Deno.Addr>(
try {
return await handler();
} catch (e) {
captureException(e, {
mechanism: {
handled: false,
type: 'auto.http.deno',
data: { function: 'serve' },
},
});
captureServeError(e);
throw e;
}
}

const integration = client.getIntegrationByName<Integration & DenoServeIntegrationOptions>('DenoServe');

const urlObject = parseStringToURLObject(request.url);
const [rawName, attributes] = getHttpSpanDetailsFromUrlObject(
urlObject,
Expand Down Expand Up @@ -122,22 +159,45 @@ export const wrapDenoRequestHandler = <Addr extends Deno.Addr = Deno.Addr>(
ipAddress: socketIp,
});

const configuredBodySize = client.getIntegrationByName<Integration & { maxRequestBodySize?: MaxRequestBodySize }>(
'DenoServe',
)?.maxRequestBodySize;
const effectiveBodySize =
configuredBodySize ?? (dataCollection.httpBodies.includes('incomingRequest') ? 'medium' : 'none');
if (request.method !== 'GET' && effectiveBodySize !== 'none') {
integration?.maxRequestBodySize ?? (dataCollection.httpBodies.includes('incomingRequest') ? 'medium' : 'none');
const ignoreRequestBody = integration?.ignoreRequestBody;
if (
request.method !== 'GET' &&
effectiveBodySize !== 'none' &&
// If the filter throws, skip the body so it cannot leak data the filter was meant to hide.
!(ignoreRequestBody && callUserCallback('ignoreRequestBody', () => ignoreRequestBody(request.url, request), true))
) {
await captureBodyFromWinterCGRequest(request, isolationScope, effectiveBodySize);
}

const urlPath = urlObject ? `${urlObject.pathname}${urlObject.search}` : '/';
const createSpan = shouldCreateSpan(client, integration, request, urlPath);
Comment thread
cursor[bot] marked this conversation as resolved.

return continueTrace(
{
sentryTrace: request.headers.get('sentry-trace') || '',
baggage: request.headers.get('baggage'),
},
() => {
async () => {
if (!createSpan) {
try {
const res = await handler();
isolationScope.setContext('response', {
status_code: res.status,
});
return res;
} catch (e) {
captureServeError(e);
throw e;
}
}

return startSpanManual({ name, attributes }, async span => {
const { onSpanCreated, onSpanEnd } = integration ?? {};
if (onSpanCreated) {
callUserCallback('onSpanCreated', () => onSpanCreated(span, request), undefined);
}
let res: Response;

try {
Expand All @@ -151,17 +211,15 @@ export const wrapDenoRequestHandler = <Addr extends Deno.Addr = Deno.Addr>(
);
} catch (e) {
span.end();
captureException(e, {
mechanism: {
handled: false,
type: 'auto.http.deno',
data: { function: 'serve' },
},
});
captureServeError(e);
throw e;
}

return streamResponse(span, res);
return streamResponse(
span,
res,
onSpanEnd && (() => callUserCallback('onSpanEnd', () => onSpanEnd(span, request, res), undefined)),
);
});
},
);
Expand Down
Loading
Loading