diff --git a/.size-limit.js b/.size-limit.js
index eed705e16da6..ca26288b07b3 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -139,7 +139,7 @@ module.exports = [
     path: 'packages/vue/build/esm/index.js',
     import: createImport('init', 'browserTracingIntegration'),
     gzip: true,
-    limit: '39.5 KB',
+    limit: '40 KB',
   },
   // Svelte SDK (ESM)
   {
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js
new file mode 100644
index 000000000000..2c85bd05b765
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+  dsn: 'https://public@dsn.ingest.sentry.io/1337',
+  integrations: [
+    Sentry.browserTracingIntegration({
+      idleTimeout: 1000,
+      onRequestSpanStart(span, { headers }) {
+        if (headers) {
+          span.setAttribute('hook.called.headers', headers.get('foo'));
+        }
+      },
+    }),
+  ],
+  tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js
new file mode 100644
index 000000000000..494ce7d23a05
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/subject.js
@@ -0,0 +1,11 @@
+fetch('http://sentry-test-site-fetch.example/', {
+  headers: {
+    foo: 'fetch',
+  },
+});
+
+const xhr = new XMLHttpRequest();
+
+xhr.open('GET', 'http://sentry-test-site-xhr.example/');
+xhr.setRequestHeader('foo', 'xhr');
+xhr.send();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts
new file mode 100644
index 000000000000..91b0c1333298
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts
@@ -0,0 +1,52 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/core';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should call onRequestSpanStart hook', async ({ browserName, getLocalTestUrl, page }) => {
+  const supportedBrowsers = ['chromium', 'firefox'];
+
+  if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+    sentryTest.skip();
+  }
+
+  await page.route('http://sentry-test-site-fetch.example/', async route => {
+    await route.fulfill({
+      status: 200,
+      contentType: 'application/json',
+      body: '',
+    });
+  });
+  await page.route('http://sentry-test-site-xhr.example/', async route => {
+    await route.fulfill({
+      status: 200,
+      contentType: 'application/json',
+      body: '',
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+
+  const envelopes = await getMultipleSentryEnvelopeRequests<Event>(page, 2, { url, timeout: 10000 });
+
+  const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers
+
+  expect(tracingEvent.spans).toContainEqual(
+    expect.objectContaining({
+      op: 'http.client',
+      data: expect.objectContaining({
+        'hook.called.headers': 'xhr',
+      }),
+    }),
+  );
+
+  expect(tracingEvent.spans).toContainEqual(
+    expect.objectContaining({
+      op: 'http.client',
+      data: expect.objectContaining({
+        'hook.called.headers': 'fetch',
+      }),
+    }),
+  );
+});
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index 062b308527d6..fab45cd1ed4f 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -9,7 +9,7 @@ import {
   startTrackingLongTasks,
   startTrackingWebVitals,
 } from '@sentry-internal/browser-utils';
-import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource } from '@sentry/core';
+import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, WebFetchHeaders } from '@sentry/core';
 import {
   GLOBAL_OBJ,
   SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON,
@@ -195,6 +195,13 @@ export interface BrowserTracingOptions {
    * Default: (url: string) => true
    */
   shouldCreateSpanForRequest?(this: void, url: string): boolean;
+
+  /**
+   * This callback is invoked directly after a span is started for an outgoing fetch or XHR request.
+   * You can use it to annotate the span with additional data or attributes, for example by setting
+   * attributes based on the passed request headers.
+   */
+  onRequestSpanStart?(span: Span, requestInformation: { headers?: WebFetchHeaders }): void;
 }
 
 const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
@@ -246,6 +253,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
     instrumentPageLoad,
     instrumentNavigation,
     linkPreviousTrace,
+    onRequestSpanStart,
   } = {
     ...DEFAULT_BROWSER_TRACING_OPTIONS,
     ..._options,
@@ -468,6 +476,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
         tracePropagationTargets: client.getOptions().tracePropagationTargets,
         shouldCreateSpanForRequest,
         enableHTTPTimings,
+        onRequestSpanStart,
       });
     },
   };
diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts
index 17dd71f0abba..0a8e42547341 100644
--- a/packages/browser/src/tracing/request.ts
+++ b/packages/browser/src/tracing/request.ts
@@ -5,7 +5,7 @@ import {
   extractNetworkProtocol,
 } from '@sentry-internal/browser-utils';
 import type { XhrHint } from '@sentry-internal/browser-utils';
-import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/core';
+import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span, WebFetchHeaders } from '@sentry/core';
 import {
   SEMANTIC_ATTRIBUTE_SENTRY_OP,
   SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -98,6 +98,11 @@ export interface RequestInstrumentationOptions {
    * Default: (url: string) => true
    */
   shouldCreateSpanForRequest?(this: void, url: string): boolean;
+
+  /**
+   * Is called when spans are started for outgoing requests.
+   */
+  onRequestSpanStart?(span: Span, requestInformation: { headers?: WebFetchHeaders }): void;
 }
 
 const responseToSpanId = new WeakMap<object, string>();
@@ -119,10 +124,9 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
     shouldCreateSpanForRequest,
     enableHTTPTimings,
     tracePropagationTargets,
+    onRequestSpanStart,
   } = {
-    traceFetch: defaultRequestInstrumentationOptions.traceFetch,
-    traceXHR: defaultRequestInstrumentationOptions.traceXHR,
-    trackFetchStreamPerformance: defaultRequestInstrumentationOptions.trackFetchStreamPerformance,
+    ...defaultRequestInstrumentationOptions,
     ..._options,
   };
 
@@ -179,10 +183,12 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
           'http.url': fullUrl,
           'server.address': host,
         });
-      }
 
-      if (enableHTTPTimings && createdSpan) {
-        addHTTPTimings(createdSpan);
+        if (enableHTTPTimings) {
+          addHTTPTimings(createdSpan);
+        }
+
+        onRequestSpanStart?.(createdSpan, { headers: handlerData.headers });
       }
     });
   }
@@ -190,8 +196,18 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
   if (traceXHR) {
     addXhrInstrumentationHandler(handlerData => {
       const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
-      if (enableHTTPTimings && createdSpan) {
-        addHTTPTimings(createdSpan);
+      if (createdSpan) {
+        if (enableHTTPTimings) {
+          addHTTPTimings(createdSpan);
+        }
+
+        let headers;
+        try {
+          headers = new Headers(handlerData.xhr.__sentry_xhr_v3__?.request_headers);
+        } catch {
+          // noop
+        }
+        onRequestSpanStart?.(createdSpan, { headers });
       }
     });
   }
diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts
index 5f0c9cc30b56..379097936ef4 100644
--- a/packages/core/src/fetch.ts
+++ b/packages/core/src/fetch.ts
@@ -4,7 +4,7 @@ import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing';
 import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan';
 import type { FetchBreadcrumbHint, HandlerDataFetch, Span, SpanAttributes, SpanOrigin } from './types-hoist';
 import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage';
-import { isInstanceOf } from './utils-hoist/is';
+import { isInstanceOf, isRequest } from './utils-hoist/is';
 import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils-hoist/url';
 import { hasSpansEnabled } from './utils/hasSpansEnabled';
 import { getActiveSpan } from './utils/spanUtils';
@@ -227,10 +227,6 @@ function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string
   );
 }
 
-function isRequest(request: unknown): request is Request {
-  return typeof Request !== 'undefined' && isInstanceOf(request, Request);
-}
-
 function isHeaders(headers: unknown): headers is Headers {
   return typeof Headers !== 'undefined' && isInstanceOf(headers, Headers);
 }
diff --git a/packages/core/src/types-hoist/instrument.ts b/packages/core/src/types-hoist/instrument.ts
index 420482579dd9..5eba6066432a 100644
--- a/packages/core/src/types-hoist/instrument.ts
+++ b/packages/core/src/types-hoist/instrument.ts
@@ -61,6 +61,8 @@ export interface HandlerDataFetch {
   error?: unknown;
   // This is to be consumed by the HttpClient integration
   virtualError?: unknown;
+  /** Headers that the user passed to the fetch request. */
+  headers?: WebFetchHeaders;
 }
 
 export interface HandlerDataDom {
diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/utils-hoist/instrument/fetch.ts
index 71c5148fae9c..7d6185d00639 100644
--- a/packages/core/src/utils-hoist/instrument/fetch.ts
+++ b/packages/core/src/utils-hoist/instrument/fetch.ts
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import type { HandlerDataFetch } from '../../types-hoist';
+import type { HandlerDataFetch, WebFetchHeaders } from '../../types-hoist';
 
-import { isError } from '../is';
+import { isError, isRequest } from '../is';
 import { addNonEnumerableProperty, fill } from '../object';
 import { supportsNativeFetch } from '../supports';
 import { timestampInSeconds } from '../time';
@@ -67,6 +67,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
         startTimestamp: timestampInSeconds() * 1000,
         // // Adding the error to be able to fingerprint the failed fetch event in HttpClient instrumentation
         virtualError,
+        headers: getHeadersFromFetchArgs(args),
       };
 
       // if there is no callback, fetch is instrumented directly
@@ -253,3 +254,26 @@ export function parseFetchArgs(fetchArgs: unknown[]): { method: string; url: str
     method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET',
   };
 }
+
+function getHeadersFromFetchArgs(fetchArgs: unknown[]): WebFetchHeaders | undefined {
+  const [requestArgument, optionsArgument] = fetchArgs;
+
+  try {
+    if (
+      typeof optionsArgument === 'object' &&
+      optionsArgument !== null &&
+      'headers' in optionsArgument &&
+      optionsArgument.headers
+    ) {
+      return new Headers(optionsArgument.headers as any);
+    }
+
+    if (isRequest(requestArgument)) {
+      return new Headers(requestArgument.headers);
+    }
+  } catch {
+    // noop
+  }
+
+  return;
+}
diff --git a/packages/core/src/utils-hoist/is.ts b/packages/core/src/utils-hoist/is.ts
index cfa9bc141e20..ab5e150e2394 100644
--- a/packages/core/src/utils-hoist/is.ts
+++ b/packages/core/src/utils-hoist/is.ts
@@ -201,3 +201,12 @@ export function isVueViewModel(wat: unknown): boolean {
   // Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property.
   return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue));
 }
+
+/**
+ * Checks whether the given parameter is a Standard Web API Request instance.
+ *
+ * Returns false if Request is not available in the current runtime.
+ */
+export function isRequest(request: unknown): request is Request {
+  return typeof Request !== 'undefined' && isInstanceOf(request, Request);
+}