diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts
index e12b03c1a9dd..ef25f4c06949 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,16 @@ 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,
+ ...options,
+ // TODO: consolidate confusingly named HTTP instrumentation options
onSpanCreated: options.onIncomingSpanCreated,
onSpanEnd: options.onIncomingSpanEnd,
errorMonitor,
- sessions: false,
});
subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest);
const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({
- spans: options.spans,
+ ...options,
breadcrumbs,
propagateTrace: tracePropagation,
ignoreOutgoingRequests: options.ignoreOutgoingRequests
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-spans-disabled.test.ts b/packages/deno/test/deno-http-spans-disabled.test.ts
new file mode 100644
index 000000000000..efd1f29755ff
--- /dev/null
+++ b/packages/deno/test/deno-http-spans-disabled.test.ts
@@ -0,0 +1,130 @@
+//
+
+/**
+ * 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 type { DenoClient } from '../build/esm/index.js';
+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[]): DenoClient {
+ getMainCarrier().__SENTRY__ = undefined;
+ return init({
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ traceLifecycle: 'static',
+ integrations: [denoHttpIntegration({ spans: false })],
+ beforeSendTransaction: (event: TransactionEvent) => {
+ 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[] = [];
+ const client = 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;
+
+ // 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');
+ 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[] = [];
+ const client = 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()));
+
+ // 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');
+ const ops = transactions.map(t => t.contexts?.trace?.op);
+ assertEquals(ops.includes('http.server'), false, `expected no http.server transaction, got ops: ${ops.join(', ')}`);
+ },
+});
diff --git a/packages/deno/test/deno-http.test.ts b/packages/deno/test/deno-http.test.ts
index 3c784576b019..0944304ac980 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 by default',
+ 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() {