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', 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/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/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/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..5508640a855c --- /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): void { + const sdkMetadata = options._metadata?.sdk; + const sdkUserAgent = + sdkMetadata?.name && sdkMetadata?.version ? `${sdkMetadata?.name}/${sdkMetadata?.version}` : undefined; + + options.transportOptions = { + ...options.transportOptions, + headers: { + ...(sdkUserAgent && { 'user-agent': sdkUserAgent }), + ...options.transportOptions?.headers, + }, + }; +} diff --git a/packages/core/src/types-hoist/transport.ts b/packages/core/src/types-hoist/transport.ts index 8e0035c93137..320ed98b00e4 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 HTTP 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..3c5fe874af9f 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, test, vi } from 'vitest'; -import { createTransport, Scope } from '../../src'; +import { applySdkMetadata, createTransport, Scope } from '../../src'; 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 +205,35 @@ describe('ServerRuntimeClient', () => { ]); }); }); + + 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..521011fea6b8 100644 --- a/packages/deno/src/transports/index.ts +++ b/packages/deno/src/transports/index.ts @@ -1,15 +1,10 @@ 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. */ -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/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: { 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;