Skip to content

Commit e35ca9d

Browse files
authored
feat(core): Send user-agent header with envelope requests in server SDKs (#17929)
As a follow-up of an incident, SDKs were asked to send a `user-agent` Http header to determine by just looking at incoming request headers which SDK the request is coming from. See [develop specification](https://develop.sentry.dev/sdk/overview/#user-agent). This PR makes the following changes to sending user agent HTTP headers with envelope requests made by the transport: - Send `user-agent` in all server-runtime SDKs - Extract the `headers` option from individual transport options to `BaseTransportOptions`. This allows us to type-safely add the user agent header to the SDKs transport options which is the easiest way to pass a header to the transport without having to modify or extend any of the existing transport APIs. I checked and every transport implementation we currently export exposed a `headers` option anyway, so this just unifies it. Given this is an optional property, custom transport implementations extending `BaseTransportOptions` won't break either. The problem here is only that they might not actually support this option which all things considered I think is fine. If reviewers have different opinions, I'm happy to revisit this. - Unit and integration/e2e tests for node and cloudflare
1 parent 14888ab commit e35ca9d

File tree

18 files changed

+112
-31
lines changed

18 files changed

+112
-31
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ module.exports = [
157157
name: 'CDN Bundle',
158158
path: createCDNPath('bundle.min.js'),
159159
gzip: true,
160-
limit: '27 KB',
160+
limit: '27.5 KB',
161161
},
162162
{
163163
name: 'CDN Bundle (incl. Tracing)',

dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError } from '@sentry-internal/test-utils';
2+
import { waitForError, waitForRequest } from '@sentry-internal/test-utils';
3+
import { SDK_VERSION } from '@sentry/cloudflare';
34
import { WebSocket } from 'ws';
45

56
test('Index page', async ({ baseURL }) => {
@@ -69,3 +70,15 @@ test('Websocket.webSocketClose', async ({ baseURL }) => {
6970
expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketClose');
7071
expect(event.exception?.values?.[0]?.mechanism?.type).toBe('auto.faas.cloudflare.durable_object');
7172
});
73+
74+
test('sends user-agent header with SDK name and version in envelope requests', async ({ baseURL }) => {
75+
const requestPromise = waitForRequest('cloudflare-workers', () => true);
76+
77+
await fetch(`${baseURL}/throwException`);
78+
79+
const request = await requestPromise;
80+
81+
expect(request.rawProxyRequestHeaders).toMatchObject({
82+
'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`,
83+
});
84+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForRequest } from '@sentry-internal/test-utils';
3+
import { SDK_VERSION } from '@sentry/node';
4+
5+
test('sends user-agent header with SDK name and version in envelope requests', async ({ baseURL }) => {
6+
const requestPromise = waitForRequest('node-express', () => true);
7+
8+
await fetch(`${baseURL}/test-exception/123`);
9+
10+
const request = await requestPromise;
11+
12+
expect(request.rawProxyRequestHeaders).toMatchObject({
13+
'user-agent': `sentry.javascript.node/${SDK_VERSION}`,
14+
});
15+
});

dev-packages/test-utils/src/event-proxy-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface EventProxyServerOptions {
2424
interface SentryRequestCallbackData {
2525
envelope: Envelope;
2626
rawProxyRequestBody: string;
27+
rawProxyRequestHeaders: Record<string, string | string[] | undefined>;
2728
rawSentryResponseBody: string;
2829
sentryResponseStatusCode?: number;
2930
}
@@ -182,6 +183,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P
182183
const data: SentryRequestCallbackData = {
183184
envelope: parseEnvelope(proxyRequestBody),
184185
rawProxyRequestBody: proxyRequestBody,
186+
rawProxyRequestHeaders: proxyRequest.headers,
185187
rawSentryResponseBody: '',
186188
sentryResponseStatusCode: 200,
187189
};

packages/browser/src/transports/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,4 @@ import type { BaseTransportOptions } from '@sentry/core';
33
export interface BrowserTransportOptions extends BaseTransportOptions {
44
/** Fetch API init parameters. Used by the FetchTransport */
55
fetchOptions?: RequestInit;
6-
/** Custom headers for the transport. Used by the XHRTransport and FetchTransport */
7-
headers?: { [key: string]: string };
86
}

packages/bun/src/transports/index.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core';
22
import { createTransport, suppressTracing } from '@sentry/core';
33

4-
export interface BunTransportOptions extends BaseTransportOptions {
5-
/** Custom headers for the transport. Used by the XHRTransport and FetchTransport */
6-
headers?: { [key: string]: string };
7-
}
8-
94
/**
105
* Creates a Transport that uses the Fetch API to send events to Sentry.
116
*/
12-
export function makeFetchTransport(options: BunTransportOptions): Transport {
7+
export function makeFetchTransport(options: BaseTransportOptions): Transport {
138
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> {
149
const requestOptions: RequestInit = {
1510
body: request.body,

packages/bun/src/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/core';
2-
import type { BunTransportOptions } from './transports';
1+
import type { BaseTransportOptions, ClientOptions, Options, TracePropagationTargets } from '@sentry/core';
32

43
export interface BaseBunOptions {
54
/**
@@ -43,10 +42,10 @@ export interface BaseBunOptions {
4342
* Configuration options for the Sentry Bun SDK
4443
* @see @sentry/core Options for more information.
4544
*/
46-
export interface BunOptions extends Options<BunTransportOptions>, BaseBunOptions {}
45+
export interface BunOptions extends Options<BaseTransportOptions>, BaseBunOptions {}
4746

4847
/**
4948
* Configuration options for the Sentry Bun SDK Client class
5049
* @see BunClient for more information.
5150
*/
52-
export interface BunClientOptions extends ClientOptions<BunTransportOptions>, BaseBunOptions {}
51+
export interface BunClientOptions extends ClientOptions<BaseTransportOptions>, BaseBunOptions {}

packages/cloudflare/src/transport.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { createTransport, SENTRY_BUFFER_FULL_ERROR, suppressTracing } from '@sen
44
export interface CloudflareTransportOptions extends BaseTransportOptions {
55
/** Fetch API init parameters. */
66
fetchOptions?: RequestInit;
7-
/** Custom headers for the transport. */
8-
headers?: { [key: string]: string };
97
}
108

119
const DEFAULT_TRANSPORT_BUFFER_SIZE = 30;

packages/core/src/server-runtime-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getIsolationScope } from './currentScopes';
44
import { DEBUG_BUILD } from './debug-build';
55
import type { Scope } from './scope';
66
import { registerSpanErrorInstrumentation } from './tracing';
7+
import { addUserAgentToTransportHeaders } from './transports/userAgent';
78
import type { CheckIn, MonitorConfig, SerializedCheckIn } from './types-hoist/checkin';
89
import type { Event, EventHint } from './types-hoist/event';
910
import type { ClientOptions } from './types-hoist/options';
@@ -36,6 +37,8 @@ export class ServerRuntimeClient<
3637
// Server clients always support tracing
3738
registerSpanErrorInstrumentation();
3839

40+
addUserAgentToTransportHeaders(options);
41+
3942
super(options);
4043
}
4144

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { ClientOptions } from '../types-hoist/options';
2+
3+
/**
4+
* Takes the SDK metadata and adds the user-agent header to the transport options.
5+
* This ensures that the SDK sends the user-agent header with SDK name and version to
6+
* all requests made by the transport.
7+
*
8+
* @see https://develop.sentry.dev/sdk/overview/#user-agent
9+
*/
10+
export function addUserAgentToTransportHeaders(options: ClientOptions): void {
11+
const sdkMetadata = options._metadata?.sdk;
12+
const sdkUserAgent =
13+
sdkMetadata?.name && sdkMetadata?.version ? `${sdkMetadata?.name}/${sdkMetadata?.version}` : undefined;
14+
15+
options.transportOptions = {
16+
...options.transportOptions,
17+
headers: {
18+
...(sdkUserAgent && { 'user-agent': sdkUserAgent }),
19+
...options.transportOptions?.headers,
20+
},
21+
};
22+
}

0 commit comments

Comments
 (0)