diff --git a/packages/deno/src/denoVersion.ts b/packages/deno/src/denoVersion.ts
new file mode 100644
index 000000000000..8d2f0d1790a7
--- /dev/null
+++ b/packages/deno/src/denoVersion.ts
@@ -0,0 +1,22 @@
+import { parseSemver } from '@sentry/core';
+
+export const DENO_VERSION = parseSemver(typeof Deno !== 'undefined' ? (Deno.version?.deno ?? '') : '') as {
+ major: number | undefined;
+ minor: number | undefined;
+ patch: number | undefined;
+};
+
+/** Exported for testing */
+function gte(major: number, minor: number, patch: number): boolean {
+ const { major: M, minor: m, patch: p } = DENO_VERSION;
+ if (M === undefined || m === undefined || p === undefined) return false;
+ if (M !== major) return M > major;
+ if (m !== minor) return m > minor;
+ return p >= patch;
+}
+
+/** Whether `http.client.request.created` fires (Deno 2.7.13+). */
+export const HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 7, 13);
+
+/** Whether `http.server.request.start` fires (Deno 2.8.0+). */
+export const HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 8, 0);
diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts
index 424aad03d3d3..d06f05f8e4f1 100644
--- a/packages/deno/src/index.ts
+++ b/packages/deno/src/index.ts
@@ -104,6 +104,8 @@ export { DenoClient } from './client';
export { getDefaultIntegrations, init } from './sdk';
export { denoServeIntegration } from './integrations/deno-serve';
+export { denoHttpIntegration } from './integrations/http';
+export type { DenoHttpIntegrationOptions } from './integrations/http';
export { denoContextIntegration } from './integrations/context';
export { globalHandlersIntegration } from './integrations/globalhandlers';
export { normalizePathsIntegration } from './integrations/normalizepaths';
diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts
new file mode 100644
index 000000000000..d09f0f3ba356
--- /dev/null
+++ b/packages/deno/src/integrations/http.ts
@@ -0,0 +1,170 @@
+import { subscribe } from 'node:diagnostics_channel';
+import { errorMonitor } from 'node:events';
+import type { ClientRequest, RequestOptions } from 'node:http';
+import type { HttpIncomingMessage, Integration, IntegrationFn, Span } from '@sentry/core';
+import {
+ debug,
+ defineIntegration,
+ getHttpClientSubscriptions,
+ getHttpServerSubscriptions,
+ getRequestOptions,
+ HTTP_ON_CLIENT_REQUEST,
+ HTTP_ON_SERVER_REQUEST,
+} from '@sentry/core';
+import { setAsyncLocalStorageAsyncContextStrategy } from '../async';
+import {
+ DENO_VERSION,
+ HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
+ HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED,
+} from '../denoVersion';
+
+const INTEGRATION_NAME = 'DenoHttp';
+
+export interface DenoHttpIntegrationOptions {
+ /**
+ * Whether breadcrumbs should be recorded for outgoing requests.
+ *
+ * @default `true`
+ */
+ breadcrumbs?: boolean;
+
+ /**
+ * Whether to create spans for incoming and outgoing HTTP requests.
+ * Defaults to the client's tracing configuration (`hasSpansEnabled`).
+ */
+ spans?: boolean;
+
+ /**
+ * Whether to inject trace propagation headers (sentry-trace, baggage) into outgoing HTTP requests.
+ *
+ * When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs
+ * (if `breadcrumbs` is enabled).
+ *
+ * @default `true`
+ */
+ tracePropagation?: boolean;
+
+ /**
+ * Whether to automatically ignore common static asset requests (favicon.ico, robots.txt, etc.)
+ * when creating server spans.
+ *
+ * @default `true`
+ */
+ ignoreStaticAssets?: boolean;
+
+ /**
+ * Controls the maximum size of incoming HTTP request bodies attached to events.
+ *
+ * @default 'medium'
+ */
+ maxRequestBodySize?: 'none' | 'small' | 'medium' | 'always';
+
+ /**
+ * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
+ *
+ * The `request` parameter is the incoming `node:http` {@link IncomingMessage} — use `request.url`,
+ * `request.method`, `request.headers`, etc.
+ */
+ ignoreRequestBody?: (url: string, request: HttpIncomingMessage) => boolean;
+
+ /**
+ * Do not capture server spans for incoming HTTP requests whose URL path makes the given callback return `true`.
+ *
+ * The `request` parameter is the incoming `node:http` {@link IncomingMessage} — use `request.url`,
+ * `request.method`, `request.headers`, etc.
+ */
+ ignoreIncomingRequests?: (urlPath: string, request: HttpIncomingMessage) => boolean;
+
+ /**
+ * Do not capture breadcrumbs, spans, or propagate trace headers for outgoing HTTP requests where the given callback returns `true`.
+ *
+ * The `request` parameter is the outgoing {@link RequestOptions} — use `request.hostname`, `request.path`,
+ * `request.method`, `request.headers`, etc.
+ */
+ ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean;
+
+ /**
+ * Hook invoked after the server span is created but before the request is handled.
+ */
+ onIncomingSpanCreated?: (span: Span, request: unknown, response: unknown) => void;
+
+ /**
+ * Hook invoked when the server span ends, before it is recorded.
+ */
+ onIncomingSpanEnd?: (span: Span, request: unknown, response: unknown) => void;
+}
+
+const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => {
+ const breadcrumbs = options.breadcrumbs ?? true;
+ const tracePropagation = options.tracePropagation ?? true;
+
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ const denoVersion = DENO_VERSION.major !== undefined ? `${Deno.version.deno}` : 'unknown';
+
+ // Below 2.7.13 neither channel fires. Warn and bail without touching the ACS.
+ if (!HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED && !HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) {
+ debug.warn(
+ `denoHttpIntegration requires Deno 2.7.13+ (client) or 2.8.0+ (server) for node:http diagnostics channels; running on Deno ${denoVersion}. The integration is a no-op on this version.`,
+ );
+ return;
+ }
+
+ // Wire up Deno's AsyncLocalStorage-backed ACS so the server subscription's
+ // `withIsolationScope(clone, ...)` actually activates the cloned scope.
+ // Without this, request isolation and span creation degrade silently.
+ setAsyncLocalStorageAsyncContextStrategy();
+
+ if (HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) {
+ const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({
+ // `spans` falls through to the client's tracing config when unset.
+ spans: options.spans,
+ ignoreStaticAssets: options.ignoreStaticAssets,
+ ignoreIncomingRequests: options.ignoreIncomingRequests,
+ maxRequestBodySize: options.maxRequestBodySize ?? 'medium',
+ ignoreRequestBody: options.ignoreRequestBody,
+ onSpanCreated: options.onIncomingSpanCreated,
+ onSpanEnd: options.onIncomingSpanEnd,
+ errorMonitor,
+ sessions: false,
+ });
+ subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest);
+ } else {
+ debug.log(
+ `denoHttpIntegration: server-side instrumentation requires Deno 2.8.0+; running on Deno ${denoVersion}. Client-side instrumentation is still active.`,
+ );
+ }
+
+ if (HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED) {
+ const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({
+ spans: options.spans,
+ breadcrumbs,
+ propagateTrace: tracePropagation,
+ ignoreOutgoingRequests: options.ignoreOutgoingRequests
+ ? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request as ClientRequest))
+ : undefined,
+ // Deno doesn't run OTel's http instrumentation, so there's no
+ // double-wrap to detect; skip the warning to avoid loading the module.
+ suppressOtelWarning: true,
+ errorMonitor,
+ });
+ subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequest);
+ }
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Instruments incoming and outgoing HTTP requests handled via the `node:http` module in Deno.
+ *
+ * Listens on Deno's `node:diagnostics_channel` for `http.server.request.start` and
+ * `http.client.request.created`, then routes them through Sentry core's portable subscription
+ * helpers (`getHttpServerSubscriptions`, `getHttpClientSubscriptions`) to create root server
+ * spans, instrument client requests, and propagate distributed trace headers.
+ *
+ * For Deno-native `Deno.serve(...)` instrumentation, see {@link denoServeIntegration}.
+ */
+export const denoHttpIntegration = defineIntegration(_denoHttpIntegration) as (
+ options?: DenoHttpIntegrationOptions,
+) => Integration & { name: 'DenoHttp'; setupOnce: () => void };
diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts
index 177c2e91234d..62e7dc6a2e70 100644
--- a/packages/deno/src/sdk.ts
+++ b/packages/deno/src/sdk.ts
@@ -16,7 +16,9 @@ import { DenoClient } from './client';
import { breadcrumbsIntegration } from './integrations/breadcrumbs';
import { denoContextIntegration } from './integrations/context';
import { contextLinesIntegration } from './integrations/contextlines';
+import { HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED } from './denoVersion';
import { denoServeIntegration } from './integrations/deno-serve';
+import { denoHttpIntegration } from './integrations/http';
import { globalHandlersIntegration } from './integrations/globalhandlers';
import { normalizePathsIntegration } from './integrations/normalizepaths';
import { setupOpenTelemetryTracer } from './opentelemetry/tracer';
@@ -39,6 +41,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
breadcrumbsIntegration(),
denoContextIntegration(),
denoServeIntegration(),
+ // node:http client diagnostics channels fire on Deno 2.7.13+
+ // server channels arrive at 2.8.0+
+ // Include in defaults if at least one is available
+ ...(HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED || HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED
+ ? [denoHttpIntegration()]
+ : []),
contextLinesIntegration(),
normalizePathsIntegration(),
globalHandlersIntegration(),
diff --git a/packages/deno/test/deno-http.test.ts b/packages/deno/test/deno-http.test.ts
new file mode 100644
index 000000000000..a67be1fc5d71
--- /dev/null
+++ b/packages/deno/test/deno-http.test.ts
@@ -0,0 +1,198 @@
+//
+
+import * as http from 'node:http';
+import type { TransactionEvent } from '@sentry/core';
+import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
+import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
+import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
+import type { DenoClient } from '../build/esm/index.js';
+import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '../build/esm/index.js';
+import {
+ DENO_VERSION,
+ HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
+ HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED,
+} from '../build/esm/denoVersion.js';
+
+function resetGlobals(): void {
+ getCurrentScope().clear();
+ getCurrentScope().setClient(undefined);
+ getIsolationScope().clear();
+ getGlobalScope().clear();
+}
+
+/**
+ * `beforeSendTransaction` hook plus a `waitFor(predicate)` helper
+ * resolves when a matching transaction arrives (or has already arrived)
+ */
+function transactionSink(): {
+ transactions: TransactionEvent[];
+ beforeSendTransaction: (event: TransactionEvent) => null;
+ waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise;
+} {
+ const transactions: TransactionEvent[] = [];
+ const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
+ return {
+ transactions,
+ beforeSendTransaction(event) {
+ transactions.push(event);
+ for (let i = waiters.length - 1; i >= 0; i--) {
+ const w = waiters[i]!;
+ if (w.predicate(event)) {
+ waiters.splice(i, 1);
+ w.resolve(event);
+ }
+ }
+ return null;
+ },
+ waitFor(predicate) {
+ const already = transactions.find(predicate);
+ if (already) return Promise.resolve(already);
+ return new Promise(resolve => {
+ waiters.push({ predicate, resolve });
+ });
+ },
+ };
+}
+
+// Bind a promise so a real "never arrives" bug fails the test.
+function withTimeout(p: Promise, ms: number, what: string): Promise {
+ let timer: ReturnType | undefined;
+ const timeout = new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
+ });
+ // Clear the timer on either resolution so Deno's leak detector is happy.
+ return Promise.race([p, timeout]).finally(() => {
+ if (timer !== undefined) clearTimeout(timer);
+ });
+}
+
+// Activation gate — split into two skip-mirrored tests so each run exercises
+// exactly one assertion. CI on a supported Deno verifies inclusion; CI on an
+// unsupported Deno verifies exclusion.
+Deno.test({
+ name: 'denoHttpIntegration: included in default integrations on Deno >= 2.7.13',
+ ignore: !HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
+ fn() {
+ resetGlobals();
+ const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
+ const names = client.getOptions().integrations.map(i => i.name);
+ assert(
+ names.includes('DenoHttp'),
+ `DenoHttp should be a default integration on Deno ${DENO_VERSION.major}.${DENO_VERSION.minor}.${DENO_VERSION.patch}, got ${names.join(', ')}`,
+ );
+ },
+});
+
+Deno.test({
+ name: 'denoHttpIntegration: NOT in default integrations on Deno < 2.7.13',
+ ignore: HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
+ fn() {
+ resetGlobals();
+ const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
+ const names = client.getOptions().integrations.map(i => i.name);
+ assert(
+ !names.includes('DenoHttp'),
+ `DenoHttp should NOT be in defaults on Deno ${DENO_VERSION.major}.${DENO_VERSION.minor}.${DENO_VERSION.patch} (< 2.7.13), got ${names.join(', ')}`,
+ );
+ },
+});
+
+Deno.test({
+ name: 'denoHttpIntegration: node:http incoming request creates an http.server transaction',
+ ignore: !HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED,
+ async fn() {
+ resetGlobals();
+ const sink = transactionSink();
+ init({
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ beforeSendTransaction: sink.beforeSendTransaction,
+ });
+
+ const server = http.createServer((_req, res) => {
+ res.end('ok');
+ });
+ const port: number = await new Promise(resolve => {
+ server.listen(0, '127.0.0.1', () => {
+ resolve((server.address() as { port: number }).port);
+ });
+ });
+
+ const response = await fetch(`http://127.0.0.1:${port}/users/42?x=1`);
+ assertEquals(await response.text(), 'ok');
+
+ // Wait on the real completion signal (transaction event flowed through
+ // beforeSendTransaction), not a fixed sleep. Bounded so a "never arrives"
+ // regression fails the test instead of hanging.
+ const txn = await withTimeout(
+ sink.waitFor(t => t.contexts?.trace?.op === 'http.server'),
+ 5000,
+ 'http.server transaction',
+ );
+
+ await new Promise(resolve => server.close(() => resolve()));
+
+ assertEquals(txn.transaction, 'GET /users/42');
+ assertEquals(txn.contexts?.trace?.data?.['http.method'], 'GET');
+ assertEquals(txn.contexts?.trace?.data?.['http.response.status_code'], 200);
+ },
+});
+
+Deno.test({
+ name: 'denoHttpIntegration: node:http outgoing request creates a child http.client span',
+ ignore: !HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
+ async fn() {
+ resetGlobals();
+ const sink = transactionSink();
+ init({
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ beforeSendTransaction: sink.beforeSendTransaction,
+ });
+
+ // Use Deno.serve for the target so the test does not depend on the
+ // node:http server side (which only works on Deno 2.8.0+).
+ const abortController = new AbortController();
+ let onListen: ((_: unknown) => void) | undefined;
+ const listening = new Promise(resolve => (onListen = resolve));
+ const target = Deno.serve(
+ { port: 0, signal: abortController.signal, onListen, hostname: '127.0.0.1' },
+ () => new Response('pong'),
+ );
+ await listening;
+ const targetPort = target.addr.port;
+
+ // Make the outgoing node:http request inside an explicit parent span so
+ // the http.client child span has somewhere to attach and txn is captured
+ await startSpan({ name: 'parent', op: 'test' }, async () => {
+ await new Promise((resolve, reject) => {
+ const req = http.request({ host: '127.0.0.1', port: targetPort, path: '/ping', method: 'GET' }, res => {
+ res.on('data', () => {});
+ res.on('end', () => resolve());
+ res.on('error', reject);
+ });
+ req.on('error', reject);
+ req.end();
+ });
+ });
+
+ // Wait on the real completion signal
+ // Note: Deno.serve's own http.server transaction may arrive first
+ const parent = await withTimeout(
+ sink.waitFor(t => t.transaction === 'parent'),
+ 5000,
+ "'parent' transaction",
+ );
+
+ abortController.abort();
+ await target.finished;
+
+ const httpClientSpan = parent.spans?.find(s => s.op === 'http.client');
+ assertExists(
+ httpClientSpan,
+ `expected an http.client child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
+ );
+ assertEquals(httpClientSpan!.data?.['http.method'], 'GET');
+ assertEquals(httpClientSpan!.data?.['http.response.status_code'], 200);
+ },
+});