From 7910c3109dae4f451b8b2c6d7cbc6e62b103d1b9 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 14 Oct 2025 16:54:26 +0200 Subject: [PATCH 1/8] feat(core): Send user-agent header with envelope requests --- .../suites/transport/userAgent/init.js | 12 ++ .../suites/transport/userAgent/subject.js | 1 + .../suites/transport/userAgent/test.ts | 32 +++++ .../cloudflare-workers/tests/index.test.ts | 15 +- .../node-express/tests/misc.test.ts | 15 ++ .../test-utils/src/event-proxy-server.ts | 2 + packages/browser/src/client.ts | 3 + packages/browser/src/transports/types.ts | 2 - packages/browser/test/client.test.ts | 32 ++++- packages/bun/src/transports/index.ts | 7 +- packages/bun/src/types.ts | 7 +- packages/cloudflare/src/transport.ts | 2 - packages/core/src/index.ts | 1 + packages/core/src/server-runtime-client.ts | 3 + packages/core/src/transports/userAgent.ts | 22 +++ packages/core/src/types-hoist/transport.ts | 5 + .../test/lib/server-runtime-client.test.ts | 136 +++++++++++++++++- packages/deno/src/transports/index.ts | 2 +- packages/deno/src/types.ts | 7 +- packages/node-core/src/transports/http.ts | 2 - packages/vercel-edge/src/transports/index.ts | 2 - 21 files changed, 284 insertions(+), 26 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/transport/userAgent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express/tests/misc.test.ts create mode 100644 packages/core/src/transports/userAgent.ts diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/init.js b/dev-packages/browser-integration-tests/suites/transport/userAgent/init.js new file mode 100644 index 000000000000..7c3376f4daa5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/userAgent/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transportOptions: { + headers: { + 'x-custom-header': 'custom-value', + }, + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js b/dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js new file mode 100644 index 000000000000..3a6f603d25b9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js @@ -0,0 +1 @@ +Sentry.captureException(new Error('test')); diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts new file mode 100644 index 000000000000..39787c89c285 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; +import { SDK_VERSION } from '@sentry/browser'; +import { sentryTest } from '../../../utils/fixtures'; + +sentryTest('adds X-Sentry-User-Agent header to envelope requests', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requestHeadersPromise = new Promise>(resolve => { + page.route('https://dsn.ingest.sentry.io/**/*', (route, request) => { + resolve(request.headers()); + return route.fulfill({ + status: 200, + body: JSON.stringify({}), + }); + }); + }); + + await page.goto(url); + + const requestHeaders = await requestHeadersPromise; + + expect(requestHeaders).toMatchObject({ + // this is the browser's user-agent header (which we don't modify) + 'user-agent': expect.any(String), + + // this is the SDK's user-agent header (in browser) + 'x-sentry-user-agent': `sentry.javascript.browser/${SDK_VERSION}`, + + // this is a custom header users add via `transportOptions.headers` + 'x-custom-header': 'custom-value', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts index ad63c1a0d307..8c09693c81ed 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForRequest } from '@sentry-internal/test-utils'; +import { SDK_VERSION } from '@sentry/cloudflare'; import { WebSocket } from 'ws'; test('Index page', async ({ baseURL }) => { @@ -69,3 +70,15 @@ test('Websocket.webSocketClose', async ({ baseURL }) => { expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketClose'); expect(event.exception?.values?.[0]?.mechanism?.type).toBe('auto.faas.cloudflare.durable_object'); }); + +test('sends user-agent header with SDK name and version in envelope requests', async ({ baseURL }) => { + const requestPromise = waitForRequest('cloudflare-workers', () => true); + + await fetch(`${baseURL}/throwException`); + + const request = await requestPromise; + + expect(request.rawProxyRequestHeaders).toMatchObject({ + 'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/misc.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/misc.test.ts new file mode 100644 index 000000000000..427c70b6fa21 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/misc.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from '@playwright/test'; +import { waitForRequest } from '@sentry-internal/test-utils'; +import { SDK_VERSION } from '@sentry/node'; + +test('sends user-agent header with SDK name and version in envelope requests', async ({ baseURL }) => { + const requestPromise = waitForRequest('node-express', () => true); + + await fetch(`${baseURL}/test-exception/123`); + + const request = await requestPromise; + + expect(request.rawProxyRequestHeaders).toMatchObject({ + 'user-agent': `sentry.javascript.node/${SDK_VERSION}`, + }); +}); diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 0becf5a743f2..08fa39db950f 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -24,6 +24,7 @@ interface EventProxyServerOptions { interface SentryRequestCallbackData { envelope: Envelope; rawProxyRequestBody: string; + rawProxyRequestHeaders: Record; rawSentryResponseBody: string; sentryResponseStatusCode?: number; } @@ -182,6 +183,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const data: SentryRequestCallbackData = { envelope: parseEnvelope(proxyRequestBody), rawProxyRequestBody: proxyRequestBody, + rawProxyRequestHeaders: proxyRequest.headers, rawSentryResponseBody: '', sentryResponseStatusCode: 200, }; diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 1b4289d66992..790af2e83e8d 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -13,6 +13,7 @@ import { _INTERNAL_flushLogsBuffer, _INTERNAL_flushMetricsBuffer, addAutoIpAddressToSession, + addUserAgentToTransportHeaders, applySdkMetadata, Client, getSDKSource, @@ -93,6 +94,8 @@ export class BrowserClient extends Client { const sdkSource = WINDOW.SENTRY_SDK_SOURCE || getSDKSource(); applySdkMetadata(opts, 'browser', ['browser'], sdkSource); + addUserAgentToTransportHeaders(opts, 'x-Sentry-User-Agent'); + // Only allow IP inferral by Relay if sendDefaultPii is true if (opts._metadata?.sdk) { opts._metadata.sdk.settings = { diff --git a/packages/browser/src/transports/types.ts b/packages/browser/src/transports/types.ts index fd8c4a93fdd6..a304e9f93d66 100644 --- a/packages/browser/src/transports/types.ts +++ b/packages/browser/src/transports/types.ts @@ -3,6 +3,4 @@ import type { BaseTransportOptions } from '@sentry/core'; export interface BrowserTransportOptions extends BaseTransportOptions { /** Fetch API init parameters. Used by the FetchTransport */ fetchOptions?: RequestInit; - /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ - headers?: { [key: string]: string }; } diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index d99e45984f0a..690476d66177 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -3,7 +3,7 @@ */ import * as sentryCore from '@sentry/core'; -import { Scope } from '@sentry/core'; +import { Scope, SDK_VERSION } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; @@ -235,3 +235,33 @@ describe('SDK metadata', () => { }); }); }); + +describe('user agent header', () => { + it('adds X-Sentry-User-Agent header to transport options', () => { + const options = getDefaultBrowserClientOptions({}); + const client = new BrowserClient(options); + + expect(client.getOptions().transportOptions?.headers).toEqual({ + 'x-Sentry-User-Agent': `sentry.javascript.browser/${SDK_VERSION}`, + }); + }); + + it('respects user-passed headers', () => { + const options = getDefaultBrowserClientOptions({ + transportOptions: { + headers: { + 'x-custom-header': 'custom-value', + 'x-Sentry-User-Agent': 'custom-user-agent', + 'user-agent': 'custom-user-agent-2', + }, + }, + }); + const client = new BrowserClient(options); + + expect(client.getOptions().transportOptions?.headers).toEqual({ + 'x-custom-header': 'custom-value', + 'x-Sentry-User-Agent': 'custom-user-agent', + 'user-agent': 'custom-user-agent-2', + }); + }); +}); diff --git a/packages/bun/src/transports/index.ts b/packages/bun/src/transports/index.ts index 7a27846548b3..20df5bb4b521 100644 --- a/packages/bun/src/transports/index.ts +++ b/packages/bun/src/transports/index.ts @@ -1,15 +1,10 @@ import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core'; import { createTransport, suppressTracing } from '@sentry/core'; -export interface BunTransportOptions extends BaseTransportOptions { - /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ - headers?: { [key: string]: string }; -} - /** * Creates a Transport that uses the Fetch API to send events to Sentry. */ -export function makeFetchTransport(options: BunTransportOptions): Transport { +export function makeFetchTransport(options: BaseTransportOptions): Transport { function makeRequest(request: TransportRequest): PromiseLike { const requestOptions: RequestInit = { body: request.body, diff --git a/packages/bun/src/types.ts b/packages/bun/src/types.ts index 755eb3e48a0a..afec75d1ee8d 100644 --- a/packages/bun/src/types.ts +++ b/packages/bun/src/types.ts @@ -1,5 +1,4 @@ -import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/core'; -import type { BunTransportOptions } from './transports'; +import type { BaseTransportOptions, ClientOptions, Options, TracePropagationTargets } from '@sentry/core'; export interface BaseBunOptions { /** @@ -43,10 +42,10 @@ export interface BaseBunOptions { * Configuration options for the Sentry Bun SDK * @see @sentry/core Options for more information. */ -export interface BunOptions extends Options, BaseBunOptions {} +export interface BunOptions extends Options, BaseBunOptions {} /** * Configuration options for the Sentry Bun SDK Client class * @see BunClient for more information. */ -export interface BunClientOptions extends ClientOptions, BaseBunOptions {} +export interface BunClientOptions extends ClientOptions, BaseBunOptions {} diff --git a/packages/cloudflare/src/transport.ts b/packages/cloudflare/src/transport.ts index 2ac401505fbb..eee54bd7a790 100644 --- a/packages/cloudflare/src/transport.ts +++ b/packages/cloudflare/src/transport.ts @@ -4,8 +4,6 @@ import { createTransport, SENTRY_BUFFER_FULL_ERROR, suppressTracing } from '@sen export interface CloudflareTransportOptions extends BaseTransportOptions { /** Fetch API init parameters. */ fetchOptions?: RequestInit; - /** Custom headers for the transport. */ - headers?: { [key: string]: string }; } const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2377e2ce86b0..604572b7060b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ export type { ClientClass as SentryCoreCurrentScopes } from './sdk'; export type { AsyncContextStrategy } from './asyncContext/types'; export type { Carrier } from './carrier'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; +export { addUserAgentToTransportHeaders } from './transports/userAgent'; export type { ServerRuntimeClientOptions } from './server-runtime-client'; export type { IntegrationIndex } from './integration'; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 9d037eb3b7c3..988e642d0a27 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -4,6 +4,7 @@ import { getIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Scope } from './scope'; import { registerSpanErrorInstrumentation } from './tracing'; +import { addUserAgentToTransportHeaders } from './transports/userAgent'; import type { CheckIn, MonitorConfig, SerializedCheckIn } from './types-hoist/checkin'; import type { Event, EventHint } from './types-hoist/event'; import type { ClientOptions } from './types-hoist/options'; @@ -36,6 +37,8 @@ export class ServerRuntimeClient< // Server clients always support tracing registerSpanErrorInstrumentation(); + addUserAgentToTransportHeaders(options); + super(options); } diff --git a/packages/core/src/transports/userAgent.ts b/packages/core/src/transports/userAgent.ts new file mode 100644 index 000000000000..53f8b0fbe5e5 --- /dev/null +++ b/packages/core/src/transports/userAgent.ts @@ -0,0 +1,22 @@ +import type { ClientOptions } from '../types-hoist/options'; + +/** + * Takes the SDK metadata and adds the user-agent header to the transport options. + * This ensures that the SDK sends the user-agent header with SDK name and version to + * all requests made by the transport. + * + * @see https://develop.sentry.dev/sdk/overview/#user-agent + */ +export function addUserAgentToTransportHeaders(options: ClientOptions, userAgentHeaderName = 'user-agent'): void { + const sdkMetadata = options._metadata?.sdk; + const sdkUserAgent = + sdkMetadata?.name && sdkMetadata?.version ? `${sdkMetadata?.name}/${sdkMetadata?.version}` : undefined; + + options.transportOptions = { + ...options.transportOptions, + headers: { + ...(sdkUserAgent && { [userAgentHeaderName]: sdkUserAgent }), + ...options.transportOptions?.headers, + }, + }; +} diff --git a/packages/core/src/types-hoist/transport.ts b/packages/core/src/types-hoist/transport.ts index 8e0035c93137..69e614dd0dd8 100644 --- a/packages/core/src/types-hoist/transport.ts +++ b/packages/core/src/types-hoist/transport.ts @@ -30,6 +30,11 @@ export interface BaseTransportOptions extends InternalBaseTransportOptions { // transport does not care about dsn specific - client should take care of // parsing and figuring that out url: string; + + /** + * Custom headers to be added to requests made by the transport. + */ + headers?: { [key: string]: string }; } export interface Transport { diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index 525ee514c1a2..ca70b781cf74 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, test, vi } from 'vitest'; -import { createTransport, Scope } from '../../src'; +import { applySdkMetadata, createTransport, Scope } from '../../src'; +import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '../../src/logs/internal'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; import type { Event, EventHint } from '../../src/types-hoist/event'; @@ -205,4 +206,137 @@ describe('ServerRuntimeClient', () => { ]); }); }); + + describe('log weight-based flushing', () => { + it('flushes logs when weight exceeds 800KB', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a large log message that will exceed the 800KB threshold + const largeMessage = 'x'.repeat(400_000); // 400KB string + _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(client['_logWeight']).toBe(0); // Weight should be reset after flush + }); + + it('accumulates log weight without flushing when under threshold', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a log message that won't exceed the threshold + const message = 'x'.repeat(100_000); // 100KB string + _INTERNAL_captureLog({ message, level: 'info' }, scope); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + expect(client['_logWeight']).toBeGreaterThan(0); + }); + + it('flushes logs on flush event', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add some logs + _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); + _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); + + // Trigger flush directly + _INTERNAL_flushLogsBuffer(client); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(client['_logWeight']).toBe(0); // Weight should be reset after flush + }); + + it('does not flush logs when logs are disabled', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a large log message + const largeMessage = 'x'.repeat(400_000); + _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + expect(client['_logWeight']).toBe(0); + }); + + it('flushes logs when flush event is triggered', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add some logs + _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); + _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); + + // Trigger flush event + client.emit('flush'); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(client['_logWeight']).toBe(0); // Weight should be reset after flush + }); + }); + + describe('user-agent header', () => { + it('sends user-agent header with SDK name and version', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + + // this is done in all `init` functions of the respective SDKs: + applySdkMetadata(options, 'core'); + + client = new ServerRuntimeClient(options); + + expect(client.getOptions().transportOptions?.headers).toEqual({ + 'user-agent': 'sentry.javascript.core/0.0.0-unknown.0', + }); + }); + + it('prefers user-passed headers (including user-agent)', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + transportOptions: { headers: { 'x-custom-header': 'custom-value', 'user-agent': 'custom-user-agent' } }, + }); + + applySdkMetadata(options, 'core'); + + client = new ServerRuntimeClient(options); + + expect(client.getOptions().transportOptions?.headers).toEqual({ + 'user-agent': 'custom-user-agent', + 'x-custom-header': 'custom-value', + }); + }); + }); }); diff --git a/packages/deno/src/transports/index.ts b/packages/deno/src/transports/index.ts index c5b6594b1c4d..4d6e13540117 100644 --- a/packages/deno/src/transports/index.ts +++ b/packages/deno/src/transports/index.ts @@ -9,7 +9,7 @@ export interface DenoTransportOptions extends BaseTransportOptions { /** * Creates a Transport that uses the Fetch API to send events to Sentry. */ -export function makeFetchTransport(options: DenoTransportOptions): Transport { +export function makeFetchTransport(options: BaseTransportOptions): Transport { const url = new URL(options.url); Deno.permissions diff --git a/packages/deno/src/types.ts b/packages/deno/src/types.ts index 1659e7a635e1..69eee4ae6313 100644 --- a/packages/deno/src/types.ts +++ b/packages/deno/src/types.ts @@ -1,5 +1,4 @@ -import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/core'; -import type { DenoTransportOptions } from './transports'; +import type { BaseTransportOptions, ClientOptions, Options, TracePropagationTargets } from '@sentry/core'; export interface BaseDenoOptions { /** @@ -44,10 +43,10 @@ export interface BaseDenoOptions { * Configuration options for the Sentry Deno SDK * @see @sentry/core Options for more information. */ -export interface DenoOptions extends Options, BaseDenoOptions {} +export interface DenoOptions extends Options, BaseDenoOptions {} /** * Configuration options for the Sentry Deno SDK Client class * @see DenoClient for more information. */ -export interface DenoClientOptions extends ClientOptions, BaseDenoOptions {} +export interface DenoClientOptions extends ClientOptions, BaseDenoOptions {} diff --git a/packages/node-core/src/transports/http.ts b/packages/node-core/src/transports/http.ts index 49897dfa22b1..3319353aff14 100644 --- a/packages/node-core/src/transports/http.ts +++ b/packages/node-core/src/transports/http.ts @@ -14,8 +14,6 @@ import { HttpsProxyAgent } from '../proxy'; import type { HTTPModule } from './http-module'; export interface NodeTransportOptions extends BaseTransportOptions { - /** Define custom headers */ - headers?: Record; /** Set a proxy that should be used for outbound requests. */ proxy?: string; /** HTTPS proxy CA certificates */ diff --git a/packages/vercel-edge/src/transports/index.ts b/packages/vercel-edge/src/transports/index.ts index bb8ea807764c..668fb6a4c236 100644 --- a/packages/vercel-edge/src/transports/index.ts +++ b/packages/vercel-edge/src/transports/index.ts @@ -4,8 +4,6 @@ import { createTransport, SENTRY_BUFFER_FULL_ERROR, suppressTracing } from '@sen export interface VercelEdgeTransportOptions extends BaseTransportOptions { /** Fetch API init parameters. */ fetchOptions?: RequestInit; - /** Custom headers for the transport. */ - headers?: { [key: string]: string }; } const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; From 8b51d2e4bf9b00ef35843bea7c7f0a5862037fbb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 14 Oct 2025 17:12:17 +0200 Subject: [PATCH 2/8] casing --- .../suites/transport/userAgent/test.ts | 2 +- packages/browser/src/client.ts | 2 +- packages/browser/test/client.test.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts index 39787c89c285..3c6a128e8016 100644 --- a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts +++ b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts @@ -24,7 +24,7 @@ sentryTest('adds X-Sentry-User-Agent header to envelope requests', async ({ getL 'user-agent': expect.any(String), // this is the SDK's user-agent header (in browser) - 'x-sentry-user-agent': `sentry.javascript.browser/${SDK_VERSION}`, + 'X-Sentry-User-Agent': `sentry.javascript.browser/${SDK_VERSION}`, // this is a custom header users add via `transportOptions.headers` 'x-custom-header': 'custom-value', diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 790af2e83e8d..b2d96eefde75 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -94,7 +94,7 @@ export class BrowserClient extends Client { const sdkSource = WINDOW.SENTRY_SDK_SOURCE || getSDKSource(); applySdkMetadata(opts, 'browser', ['browser'], sdkSource); - addUserAgentToTransportHeaders(opts, 'x-Sentry-User-Agent'); + addUserAgentToTransportHeaders(opts, 'X-Sentry-User-Agent'); // Only allow IP inferral by Relay if sendDefaultPii is true if (opts._metadata?.sdk) { diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 690476d66177..eb03e5dc0a2e 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -242,7 +242,7 @@ describe('user agent header', () => { const client = new BrowserClient(options); expect(client.getOptions().transportOptions?.headers).toEqual({ - 'x-Sentry-User-Agent': `sentry.javascript.browser/${SDK_VERSION}`, + 'X-Sentry-User-Agent': `sentry.javascript.browser/${SDK_VERSION}`, }); }); @@ -251,7 +251,7 @@ describe('user agent header', () => { transportOptions: { headers: { 'x-custom-header': 'custom-value', - 'x-Sentry-User-Agent': 'custom-user-agent', + 'X-Sentry-User-Agent': 'custom-user-agent', 'user-agent': 'custom-user-agent-2', }, }, @@ -260,7 +260,7 @@ describe('user agent header', () => { expect(client.getOptions().transportOptions?.headers).toEqual({ 'x-custom-header': 'custom-value', - 'x-Sentry-User-Agent': 'custom-user-agent', + 'X-Sentry-User-Agent': 'custom-user-agent', 'user-agent': 'custom-user-agent-2', }); }); From 197134bc2e0d07e0688c8d70f4aeff8638d4d607 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 14 Oct 2025 17:23:49 +0200 Subject: [PATCH 3/8] fix rebase mistake --- .../test/lib/server-runtime-client.test.ts | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index ca70b781cf74..d46bc8152d92 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -207,108 +207,6 @@ describe('ServerRuntimeClient', () => { }); }); - describe('log weight-based flushing', () => { - it('flushes logs when weight exceeds 800KB', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Create a large log message that will exceed the 800KB threshold - const largeMessage = 'x'.repeat(400_000); // 400KB string - _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(client['_logWeight']).toBe(0); // Weight should be reset after flush - }); - - it('accumulates log weight without flushing when under threshold', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Create a log message that won't exceed the threshold - const message = 'x'.repeat(100_000); // 100KB string - _INTERNAL_captureLog({ message, level: 'info' }, scope); - - expect(sendEnvelopeSpy).not.toHaveBeenCalled(); - expect(client['_logWeight']).toBeGreaterThan(0); - }); - - it('flushes logs on flush event', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Add some logs - _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); - _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); - - // Trigger flush directly - _INTERNAL_flushLogsBuffer(client); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(client['_logWeight']).toBe(0); // Weight should be reset after flush - }); - - it('does not flush logs when logs are disabled', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Create a large log message - const largeMessage = 'x'.repeat(400_000); - _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); - - expect(sendEnvelopeSpy).not.toHaveBeenCalled(); - expect(client['_logWeight']).toBe(0); - }); - - it('flushes logs when flush event is triggered', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Add some logs - _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); - _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); - - // Trigger flush event - client.emit('flush'); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(client['_logWeight']).toBe(0); // Weight should be reset after flush - }); - }); - describe('user-agent header', () => { it('sends user-agent header with SDK name and version', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); From 1bc8b5607b9858cafb16df0e4e3d4edb0be137b2 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 15 Oct 2025 09:56:51 +0200 Subject: [PATCH 4/8] fix tests, lint --- .../suites/transport/userAgent/test.ts | 2 +- packages/core/src/types-hoist/transport.ts | 2 +- packages/core/test/lib/server-runtime-client.test.ts | 1 - packages/node-core/test/sdk/client.test.ts | 5 +++++ packages/node/test/sdk/client.test.ts | 5 +++++ 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts index 3c6a128e8016..39787c89c285 100644 --- a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts +++ b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts @@ -24,7 +24,7 @@ sentryTest('adds X-Sentry-User-Agent header to envelope requests', async ({ getL 'user-agent': expect.any(String), // this is the SDK's user-agent header (in browser) - 'X-Sentry-User-Agent': `sentry.javascript.browser/${SDK_VERSION}`, + 'x-sentry-user-agent': `sentry.javascript.browser/${SDK_VERSION}`, // this is a custom header users add via `transportOptions.headers` 'x-custom-header': 'custom-value', diff --git a/packages/core/src/types-hoist/transport.ts b/packages/core/src/types-hoist/transport.ts index 69e614dd0dd8..320ed98b00e4 100644 --- a/packages/core/src/types-hoist/transport.ts +++ b/packages/core/src/types-hoist/transport.ts @@ -32,7 +32,7 @@ export interface BaseTransportOptions extends InternalBaseTransportOptions { url: string; /** - * Custom headers to be added to requests made by the transport. + * Custom HTTP headers to be added to requests made by the transport. */ headers?: { [key: string]: string }; } diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index d46bc8152d92..3c5fe874af9f 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, test, vi } from 'vitest'; import { applySdkMetadata, createTransport, Scope } from '../../src'; -import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '../../src/logs/internal'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; import type { Event, EventHint } from '../../src/types-hoist/event'; diff --git a/packages/node-core/test/sdk/client.test.ts b/packages/node-core/test/sdk/client.test.ts index 01623f49f0a3..0bcef2669095 100644 --- a/packages/node-core/test/sdk/client.test.ts +++ b/packages/node-core/test/sdk/client.test.ts @@ -32,6 +32,11 @@ describe('NodeClient', () => { dsn: expect.any(String), integrations: [], transport: options.transport, + transportOptions: { + headers: { + 'user-agent': `sentry.javascript.node/${SDK_VERSION}`, + }, + }, stackParser: options.stackParser, _metadata: { sdk: { diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 7f57d4772212..ff58698a7931 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -31,6 +31,11 @@ describe('NodeClient', () => { dsn: expect.any(String), integrations: [], transport: options.transport, + transportOptions: { + headers: { + 'user-agent': `sentry.javascript.node/${SDK_VERSION}`, + }, + }, stackParser: options.stackParser, _metadata: { sdk: { From b4c808ca27f6f12a6049a66ee367901b2509e926 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 15 Oct 2025 09:59:28 +0200 Subject: [PATCH 5/8] size limit --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 5ccf34d416c0..10aa99dd8954 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -150,7 +150,7 @@ module.exports = [ name: 'CDN Bundle', path: createCDNPath('bundle.min.js'), gzip: true, - limit: '27 KB', + limit: '27.5 KB', }, { name: 'CDN Bundle (incl. Tracing)', @@ -183,7 +183,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '123 KB', + limit: '124 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', From 83ec806ee18b7c8ff82a518a241779c190c2a2d5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 15 Oct 2025 10:15:30 +0200 Subject: [PATCH 6/8] rm deno transport options --- packages/deno/src/transports/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/deno/src/transports/index.ts b/packages/deno/src/transports/index.ts index 4d6e13540117..521011fea6b8 100644 --- a/packages/deno/src/transports/index.ts +++ b/packages/deno/src/transports/index.ts @@ -1,11 +1,6 @@ import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core'; import { consoleSandbox, createTransport, debug, suppressTracing } from '@sentry/core'; -export interface DenoTransportOptions extends BaseTransportOptions { - /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ - headers?: { [key: string]: string }; -} - /** * Creates a Transport that uses the Fetch API to send events to Sentry. */ From 76c8191e024e477be0da2c3e5e7bd4564518732f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 15 Oct 2025 11:58:54 +0200 Subject: [PATCH 7/8] remove browser functionality due to CORS --- .../suites/transport/userAgent/init.js | 12 ------- .../suites/transport/userAgent/subject.js | 1 - .../suites/transport/userAgent/test.ts | 32 ------------------- packages/browser/src/client.ts | 3 -- packages/browser/test/client.test.ts | 30 ----------------- packages/core/src/index.ts | 1 - packages/core/src/transports/userAgent.ts | 4 +-- 7 files changed, 2 insertions(+), 81 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/transport/userAgent/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/init.js b/dev-packages/browser-integration-tests/suites/transport/userAgent/init.js deleted file mode 100644 index 7c3376f4daa5..000000000000 --- a/dev-packages/browser-integration-tests/suites/transport/userAgent/init.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - transportOptions: { - headers: { - 'x-custom-header': 'custom-value', - }, - }, -}); diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js b/dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js deleted file mode 100644 index 3a6f603d25b9..000000000000 --- a/dev-packages/browser-integration-tests/suites/transport/userAgent/subject.js +++ /dev/null @@ -1 +0,0 @@ -Sentry.captureException(new Error('test')); diff --git a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts b/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts deleted file mode 100644 index 39787c89c285..000000000000 --- a/dev-packages/browser-integration-tests/suites/transport/userAgent/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect } from '@playwright/test'; -import { SDK_VERSION } from '@sentry/browser'; -import { sentryTest } from '../../../utils/fixtures'; - -sentryTest('adds X-Sentry-User-Agent header to envelope requests', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const requestHeadersPromise = new Promise>(resolve => { - page.route('https://dsn.ingest.sentry.io/**/*', (route, request) => { - resolve(request.headers()); - return route.fulfill({ - status: 200, - body: JSON.stringify({}), - }); - }); - }); - - await page.goto(url); - - const requestHeaders = await requestHeadersPromise; - - expect(requestHeaders).toMatchObject({ - // this is the browser's user-agent header (which we don't modify) - 'user-agent': expect.any(String), - - // this is the SDK's user-agent header (in browser) - 'x-sentry-user-agent': `sentry.javascript.browser/${SDK_VERSION}`, - - // this is a custom header users add via `transportOptions.headers` - 'x-custom-header': 'custom-value', - }); -}); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index b2d96eefde75..1b4289d66992 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -13,7 +13,6 @@ import { _INTERNAL_flushLogsBuffer, _INTERNAL_flushMetricsBuffer, addAutoIpAddressToSession, - addUserAgentToTransportHeaders, applySdkMetadata, Client, getSDKSource, @@ -94,8 +93,6 @@ export class BrowserClient extends Client { const sdkSource = WINDOW.SENTRY_SDK_SOURCE || getSDKSource(); applySdkMetadata(opts, 'browser', ['browser'], sdkSource); - addUserAgentToTransportHeaders(opts, 'X-Sentry-User-Agent'); - // Only allow IP inferral by Relay if sendDefaultPii is true if (opts._metadata?.sdk) { opts._metadata.sdk.settings = { diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index eb03e5dc0a2e..19c18ace524f 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -235,33 +235,3 @@ describe('SDK metadata', () => { }); }); }); - -describe('user agent header', () => { - it('adds X-Sentry-User-Agent header to transport options', () => { - const options = getDefaultBrowserClientOptions({}); - const client = new BrowserClient(options); - - expect(client.getOptions().transportOptions?.headers).toEqual({ - 'X-Sentry-User-Agent': `sentry.javascript.browser/${SDK_VERSION}`, - }); - }); - - it('respects user-passed headers', () => { - const options = getDefaultBrowserClientOptions({ - transportOptions: { - headers: { - 'x-custom-header': 'custom-value', - 'X-Sentry-User-Agent': 'custom-user-agent', - 'user-agent': 'custom-user-agent-2', - }, - }, - }); - const client = new BrowserClient(options); - - expect(client.getOptions().transportOptions?.headers).toEqual({ - 'x-custom-header': 'custom-value', - 'X-Sentry-User-Agent': 'custom-user-agent', - 'user-agent': 'custom-user-agent-2', - }); - }); -}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 604572b7060b..2377e2ce86b0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,7 +4,6 @@ export type { ClientClass as SentryCoreCurrentScopes } from './sdk'; export type { AsyncContextStrategy } from './asyncContext/types'; export type { Carrier } from './carrier'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; -export { addUserAgentToTransportHeaders } from './transports/userAgent'; export type { ServerRuntimeClientOptions } from './server-runtime-client'; export type { IntegrationIndex } from './integration'; diff --git a/packages/core/src/transports/userAgent.ts b/packages/core/src/transports/userAgent.ts index 53f8b0fbe5e5..5508640a855c 100644 --- a/packages/core/src/transports/userAgent.ts +++ b/packages/core/src/transports/userAgent.ts @@ -7,7 +7,7 @@ import type { ClientOptions } from '../types-hoist/options'; * * @see https://develop.sentry.dev/sdk/overview/#user-agent */ -export function addUserAgentToTransportHeaders(options: ClientOptions, userAgentHeaderName = 'user-agent'): void { +export function addUserAgentToTransportHeaders(options: ClientOptions): void { const sdkMetadata = options._metadata?.sdk; const sdkUserAgent = sdkMetadata?.name && sdkMetadata?.version ? `${sdkMetadata?.name}/${sdkMetadata?.version}` : undefined; @@ -15,7 +15,7 @@ export function addUserAgentToTransportHeaders(options: ClientOptions, userAgent options.transportOptions = { ...options.transportOptions, headers: { - ...(sdkUserAgent && { [userAgentHeaderName]: sdkUserAgent }), + ...(sdkUserAgent && { 'user-agent': sdkUserAgent }), ...options.transportOptions?.headers, }, }; From cddc6e43ce6415c2952f478b9e5b20ef0e838c52 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 15 Oct 2025 12:10:42 +0200 Subject: [PATCH 8/8] cleanup --- packages/browser/test/client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 19c18ace524f..d99e45984f0a 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -3,7 +3,7 @@ */ import * as sentryCore from '@sentry/core'; -import { Scope, SDK_VERSION } from '@sentry/core'; +import { Scope } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers';