From a47c68ba7ab4ecf842d5c9ed9c200a9ae3ea446f Mon Sep 17 00:00:00 2001 From: Abhinav Gorrepati Date: Tue, 11 Aug 2026 16:56:33 -0700 Subject: [PATCH 1/6] fix(deno): Enable sessions for HTTP requests Deno disabled release-health sessions for incoming node:http requests even though the shared HTTP instrumentation defaults them on. Preserve the shared default and cover it with a real request regression test. Co-Authored-By: OpenAI Codex Signed-off-by: Abhinav Gorrepati --- packages/deno/src/integrations/http.ts | 1 - packages/deno/test/deno-http.test.ts | 51 +++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts index e12b03c1a9dd..70d16828f122 100644 --- a/packages/deno/src/integrations/http.ts +++ b/packages/deno/src/integrations/http.ts @@ -104,7 +104,6 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { onSpanCreated: options.onIncomingSpanCreated, onSpanEnd: options.onIncomingSpanEnd, errorMonitor, - sessions: false, }); subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest); diff --git a/packages/deno/test/deno-http.test.ts b/packages/deno/test/deno-http.test.ts index 3c784576b019..cc00dddeeb96 100644 --- a/packages/deno/test/deno-http.test.ts +++ b/packages/deno/test/deno-http.test.ts @@ -1,13 +1,14 @@ // import * as http from 'node:http'; -import type { TransactionEvent } from '@sentry/core'; -import { getMainCarrier } from '@sentry/core'; +import type { Envelope, SessionAggregates, TransactionEvent } from '@sentry/core'; +import { forEachEnvelopeItem, getMainCarrier } 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 { init, startSpan } from '../build/esm/index.js'; +import { makeTestTransport } from './transport.ts'; function resetGlobals(): void { getMainCarrier().__SENTRY__ = undefined; @@ -110,6 +111,52 @@ Deno.test({ }, }); +Deno.test({ + name: 'denoHttpIntegration: node:http incoming request records a release-health session', + async fn() { + resetGlobals(); + const envelopes: Envelope[] = []; + const client = init({ + dsn: 'https://username@domain/123', + release: '1.0.0', + transport: makeTestTransport(envelope => { + envelopes.push(envelope); + }), + }); + + 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}/health`); + assertEquals(await response.text(), 'ok'); + await new Promise(resolve => server.close(() => resolve())); + await client.flush(2_000); + + let sessionAggregates: SessionAggregates | undefined; + for (const envelope of envelopes) { + forEachEnvelopeItem(envelope, item => { + const [headers, body] = item; + if (headers.type === 'sessions') { + sessionAggregates = body as SessionAggregates; + } + }); + } + + assertExists(sessionAggregates); + assertEquals(sessionAggregates.attrs?.release, '1.0.0'); + assertEquals(sessionAggregates.aggregates.length, 1); + assertEquals(sessionAggregates.aggregates[0]?.exited, 1); + assertEquals(sessionAggregates.aggregates[0]?.errored, 0); + assertEquals(sessionAggregates.aggregates[0]?.crashed, 0); + }, +}); + Deno.test({ name: 'denoHttpIntegration: node:http outgoing request creates a child http.client span', async fn() { From 0e8029443a1b253203f52bb1e8faac26152f412e Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 18 Aug 2026 08:34:07 -0700 Subject: [PATCH 2/6] fixup! fix(deno): Enable sessions for HTTP requests --- packages/deno/src/integrations/http.ts | 15 ++--- .../test/deno-http-sessions-disabled.test.ts | 64 +++++++++++++++++++ packages/deno/test/deno-http.test.ts | 2 +- 3 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 packages/deno/test/deno-http-sessions-disabled.test.ts diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts index 70d16828f122..309ea676f6bf 100644 --- a/packages/deno/src/integrations/http.ts +++ b/packages/deno/src/integrations/http.ts @@ -9,11 +9,12 @@ import { getRequestOptions, HTTP_ON_CLIENT_REQUEST, HTTP_ON_SERVER_REQUEST, + type HttpInstrumentationOptions, } from '@sentry/core'; const INTEGRATION_NAME = 'DenoHttp' as const; -export interface DenoHttpIntegrationOptions { +export interface DenoHttpIntegrationOptions extends HttpInstrumentationOptions { /** * Whether breadcrumbs should be recorded for outgoing requests. * @@ -95,21 +96,15 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { name: INTEGRATION_NAME, setupOnce() { 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, - ignoreRequestBody: options.ignoreRequestBody, - onSpanCreated: options.onIncomingSpanCreated, - onSpanEnd: options.onIncomingSpanEnd, + ...options, errorMonitor, }); subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest); const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({ - spans: options.spans, + ...options, breadcrumbs, + // TODO: consolidate confusingly named HTTP instrumentation options propagateTrace: tracePropagation, ignoreOutgoingRequests: options.ignoreOutgoingRequests ? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request)) diff --git a/packages/deno/test/deno-http-sessions-disabled.test.ts b/packages/deno/test/deno-http-sessions-disabled.test.ts new file mode 100644 index 000000000000..163ec8dde196 --- /dev/null +++ b/packages/deno/test/deno-http-sessions-disabled.test.ts @@ -0,0 +1,64 @@ +// + +/** + * Lives in its own file because `setupOnce` runs once per process + * (`installedIntegrations` guards it) and the diagnostics channel + * subscription is global. Deno gives each test file a fresh module graph, + * so this is the only way to install `denoHttpIntegration` with + * non-default options after `deno-http.test.ts` has installed it with + * the defaults. + */ + +import * as http from 'node:http'; +import type { Envelope } from '@sentry/core'; +import { forEachEnvelopeItem, getIsolationScope, getMainCarrier } 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 { denoHttpIntegration, init } from '../build/esm/index.js'; +import { makeTestTransport } from './transport.ts'; + +Deno.test({ + name: 'denoHttpIntegration: node:http incoming request records no session when sessions: false', + async fn() { + getMainCarrier().__SENTRY__ = undefined; + + const envelopes: Envelope[] = []; + const client = init({ + dsn: 'https://username@domain/123', + release: '1.0.0', + integrations: [denoHttpIntegration({ sessions: false })], + transport: makeTestTransport(envelope => { + envelopes.push(envelope); + }), + }); + + // Captured inside the handler so we can tell "sessions were disabled" + // apart from "the request was never instrumented at all". + let isolatedTransactionName: string | undefined; + const server = http.createServer((_req, res) => { + isolatedTransactionName = getIsolationScope().getScopeData().transactionName; + 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}/health`); + assertEquals(await response.text(), 'ok'); + await new Promise(resolve => server.close(() => resolve())); + await client.flush(2_000); + + const itemTypes: string[] = []; + for (const envelope of envelopes) { + forEachEnvelopeItem(envelope, ([headers]) => { + itemTypes.push(headers.type); + }); + } + + assertEquals(isolatedTransactionName, 'GET /health'); + assert(!itemTypes.includes('sessions'), `expected no session envelope item, got: ${itemTypes.join(', ')}`); + assert(!itemTypes.includes('session'), `expected no session envelope item, got: ${itemTypes.join(', ')}`); + }, +}); diff --git a/packages/deno/test/deno-http.test.ts b/packages/deno/test/deno-http.test.ts index cc00dddeeb96..0944304ac980 100644 --- a/packages/deno/test/deno-http.test.ts +++ b/packages/deno/test/deno-http.test.ts @@ -112,7 +112,7 @@ Deno.test({ }); Deno.test({ - name: 'denoHttpIntegration: node:http incoming request records a release-health session', + name: 'denoHttpIntegration: node:http incoming request records a release-health session by default', async fn() { resetGlobals(); const envelopes: Envelope[] = []; From 614e1c7259459ab4784e37d7b3e4c05c59abad60 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 18 Aug 2026 08:51:15 -0700 Subject: [PATCH 3/6] fixup! fixup! fix(deno): Enable sessions for HTTP requests --- packages/deno/src/integrations/http.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts index 309ea676f6bf..ebc3fd91ef73 100644 --- a/packages/deno/src/integrations/http.ts +++ b/packages/deno/src/integrations/http.ts @@ -97,14 +97,15 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { setupOnce() { const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({ ...options, + // TODO: consolidate confusingly named HTTP instrumentation options + onSpanCreated: options.onIncomingSpanCreated, + onSpanEnd: options.onIncomingSpanEnd, errorMonitor, }); subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest); const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({ - ...options, breadcrumbs, - // TODO: consolidate confusingly named HTTP instrumentation options propagateTrace: tracePropagation, ignoreOutgoingRequests: options.ignoreOutgoingRequests ? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request)) From f940bd3c80fb6e70e6fbf398a1a27901e269a46e Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 18 Aug 2026 09:09:19 -0700 Subject: [PATCH 4/6] fixup! fixup! fixup! fix(deno): Enable sessions for HTTP requests --- packages/deno/src/integrations/http.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts index ebc3fd91ef73..ef25f4c06949 100644 --- a/packages/deno/src/integrations/http.ts +++ b/packages/deno/src/integrations/http.ts @@ -105,6 +105,7 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest); const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({ + ...options, breadcrumbs, propagateTrace: tracePropagation, ignoreOutgoingRequests: options.ignoreOutgoingRequests From ded3cdd0c461a9889d9c1f493b4e6f607280c6ee Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 18 Aug 2026 09:23:19 -0700 Subject: [PATCH 5/6] fixup! fixup! fixup! fixup! fix(deno): Enable sessions for HTTP requests --- .../test/deno-http-spans-disabled.test.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 packages/deno/test/deno-http-spans-disabled.test.ts diff --git a/packages/deno/test/deno-http-spans-disabled.test.ts b/packages/deno/test/deno-http-spans-disabled.test.ts new file mode 100644 index 000000000000..220498d89504 --- /dev/null +++ b/packages/deno/test/deno-http-spans-disabled.test.ts @@ -0,0 +1,120 @@ +// + +/** + * Lives in its own file because `setupOnce` runs once per process + * (`installedIntegrations` guards it) and the diagnostics channel + * subscription is global. Deno gives each test file a fresh module graph, + * so this is the only way to install `denoHttpIntegration` with + * `spans: false` after another file has installed it with the defaults. + * + * Both tests below share that single `spans: false` subscription. + */ + +import * as http from 'node:http'; +import type { TransactionEvent } from '@sentry/core'; +import { getIsolationScope, getMainCarrier } 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 { denoHttpIntegration, init, startSpan } from '../build/esm/index.js'; + +/** + * `spans: false` must win over `tracesSampleRate: 1`, so tracing is on + * everywhere except the HTTP integration. Without it the option would be + * indistinguishable from tracing being off. + */ +function initWithSpansDisabled(transactions: TransactionEvent[]): void { + getMainCarrier().__SENTRY__ = undefined; + init({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + traceLifecycle: 'static', + integrations: [denoHttpIntegration({ spans: false })], + beforeSendTransaction: (event: TransactionEvent) => { + transactions.push(event); + return null; + }, + }); +} + +Deno.test({ + name: 'denoHttpIntegration: node:http outgoing request creates no http.client span when spans: false', + async fn() { + const transactions: TransactionEvent[] = []; + initWithSpansDisabled(transactions); + + // Deno.serve for the target so this does not depend on the node:http + // server instrumentation. + const abortController = new AbortController(); + let onListen: ((_: unknown) => void) | undefined; + const listening = new Promise(resolve => (onListen = resolve)); + // Captured so we can tell "spans were disabled" apart from "the client + // was never instrumented at all" -- header injection survives spans: false. + let sentryTraceHeader: string | null = null; + const target = Deno.serve( + { port: 0, signal: abortController.signal, onListen, hostname: '127.0.0.1' }, + (request: Request) => { + sentryTraceHeader = request.headers.get('sentry-trace'); + return new Response('pong'); + }, + ); + await listening; + + await startSpan({ name: 'parent', op: 'test' }, async () => { + await new Promise((resolve, reject) => { + const req = http.request({ host: '127.0.0.1', port: target.addr.port, path: '/ping', method: 'GET' }, res => { + res.on('data', () => {}); + res.on('end', () => resolve()); + res.on('error', reject); + }); + req.on('error', reject); + req.end(); + }); + }); + + abortController.abort(); + await target.finished; + + // The parent span proves tracing itself is live, so an absent + // http.client span is the option working rather than tracing being off. + assert(sentryTraceHeader, 'expected an injected sentry-trace header, so the client was instrumented'); + const parent = transactions.find(t => t.transaction === 'parent'); + assert(parent, `expected the 'parent' transaction, got: ${transactions.map(t => t.transaction).join(', ')}`); + const childOps = parent!.spans?.map(s => s.op) ?? []; + assertEquals( + childOps.includes('http.client'), + false, + `expected no http.client span, got ops: ${childOps.join(', ')}`, + ); + }, +}); + +Deno.test({ + name: 'denoHttpIntegration: node:http incoming request creates no http.server transaction when spans: false', + async fn() { + const transactions: TransactionEvent[] = []; + initWithSpansDisabled(transactions); + + // Captured inside the handler so we can tell "spans were disabled" apart + // from "the request was never instrumented at all". + let isolatedTransactionName: string | undefined; + const server = http.createServer((_req, res) => { + isolatedTransactionName = getIsolationScope().getScopeData().transactionName; + 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`); + assertEquals(await response.text(), 'ok'); + await new Promise(resolve => server.close(() => resolve())); + + // Request isolation still runs with spans off, so this proves the + // instrumentation saw the request. + assertEquals(isolatedTransactionName, 'GET /users/42'); + const ops = transactions.map(t => t.contexts?.trace?.op); + assertEquals(ops.includes('http.server'), false, `expected no http.server transaction, got ops: ${ops.join(', ')}`); + }, +}); From fded04979cab82450915dd1b0057db81f0dd4ee4 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 18 Aug 2026 09:38:39 -0700 Subject: [PATCH 6/6] fixup! fixup! fixup! fixup! fixup! fix(deno): Enable sessions for HTTP requests --- .../test/deno-http-spans-disabled.test.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/deno/test/deno-http-spans-disabled.test.ts b/packages/deno/test/deno-http-spans-disabled.test.ts index 220498d89504..efd1f29755ff 100644 --- a/packages/deno/test/deno-http-spans-disabled.test.ts +++ b/packages/deno/test/deno-http-spans-disabled.test.ts @@ -15,6 +15,7 @@ import type { TransactionEvent } from '@sentry/core'; import { getIsolationScope, getMainCarrier } 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 type { DenoClient } from '../build/esm/index.js'; import { denoHttpIntegration, init, startSpan } from '../build/esm/index.js'; /** @@ -22,9 +23,9 @@ import { denoHttpIntegration, init, startSpan } from '../build/esm/index.js'; * everywhere except the HTTP integration. Without it the option would be * indistinguishable from tracing being off. */ -function initWithSpansDisabled(transactions: TransactionEvent[]): void { +function initWithSpansDisabled(transactions: TransactionEvent[]): DenoClient { getMainCarrier().__SENTRY__ = undefined; - init({ + return init({ dsn: 'https://username@domain/123', tracesSampleRate: 1, traceLifecycle: 'static', @@ -33,14 +34,14 @@ function initWithSpansDisabled(transactions: TransactionEvent[]): void { transactions.push(event); return null; }, - }); + }) as DenoClient; } Deno.test({ name: 'denoHttpIntegration: node:http outgoing request creates no http.client span when spans: false', async fn() { const transactions: TransactionEvent[] = []; - initWithSpansDisabled(transactions); + const client = initWithSpansDisabled(transactions); // Deno.serve for the target so this does not depend on the node:http // server instrumentation. @@ -74,6 +75,10 @@ Deno.test({ abortController.abort(); await target.finished; + // Event capture runs through the client's async processing queue, so + // drain it before reading the sink -- otherwise these assertions race. + await client.flush(5_000); + // The parent span proves tracing itself is live, so an absent // http.client span is the option working rather than tracing being off. assert(sentryTraceHeader, 'expected an injected sentry-trace header, so the client was instrumented'); @@ -92,7 +97,7 @@ Deno.test({ name: 'denoHttpIntegration: node:http incoming request creates no http.server transaction when spans: false', async fn() { const transactions: TransactionEvent[] = []; - initWithSpansDisabled(transactions); + const client = initWithSpansDisabled(transactions); // Captured inside the handler so we can tell "spans were disabled" apart // from "the request was never instrumented at all". @@ -111,6 +116,11 @@ Deno.test({ assertEquals(await response.text(), 'ok'); await new Promise(resolve => server.close(() => resolve())); + // Drain the async processing queue first. Without this, "no http.server + // transaction yet" and "no http.server transaction at all" look alike, + // so the assertion below could pass while spans were still enabled. + await client.flush(5_000); + // Request isolation still runs with spans off, so this proves the // instrumentation saw the request. assertEquals(isolatedTransactionName, 'GET /users/42');