Skip to content

Commit d9e6550

Browse files
committed
feat(transport): support dynamic headers in Fetch/OtlpHttp transport (#1490)
1 parent 29d090c commit d9e6550

File tree

8 files changed

+201
-6
lines changed

8 files changed

+201
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Next
44

5+
- Feature (`@grafana/faro-web-sdk`): Fetch transport now supports dynamic header values. Each header can be a static string or a function returning a string, resolved at request time (#1490).
6+
7+
- Feature (`@grafana/faro-transport-otlp-http [experimental]`): OLTP HTTP transport now supports dynamic header values. Each header can be a static string or a function returning a string, resolved at request time (#1490).
8+
59
## 2.0.2
610

711
- Breaking (`@grafana/faro-web-sdk`): User action events now have a standardized event name

experimental/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Next
44

55
- Added support for experimental `ReplayInstrumentation` (`@grafana/faro-instrumentation-replay`).
6+
- Feature (`@grafana/faro-transport-otlp-http`): OLTP HTTP transport now supports dynamic header values. Each header can be a static string or a function returning a string, resolved at request time (#1490).
67

78
## 1.13.0
89

experimental/transport-otlp-http/src/transport.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,87 @@ describe('OtlpHttpTransport', () => {
420420
expect(mockResponseTextFn).toHaveBeenCalledTimes(1);
421421
});
422422

423+
it('will add static header values', () => {
424+
const transport = new OtlpHttpTransport({
425+
logsURL: 'https://www.example.com/v1/logs',
426+
requestOptions: {
427+
headers: {
428+
Authorization: 'Bearer static-token',
429+
'X-Static': 'static-value',
430+
},
431+
},
432+
});
433+
434+
transport.internalLogger = mockInternalLogger;
435+
436+
transport.send([logTransportItem]);
437+
438+
expect(fetch).toHaveBeenCalledTimes(1);
439+
expect(fetch).toHaveBeenCalledWith(
440+
'https://www.example.com/v1/logs',
441+
expect.objectContaining({
442+
headers: expect.objectContaining({
443+
Authorization: 'Bearer static-token',
444+
'X-Static': 'static-value',
445+
}),
446+
})
447+
);
448+
});
449+
450+
it('will add dynamic header values from sync callbacks', () => {
451+
const transport = new OtlpHttpTransport({
452+
logsURL: 'https://www.example.com/v1/logs',
453+
requestOptions: {
454+
headers: {
455+
Authorization: () => 'Bearer dynamic-token',
456+
'X-User': () => 'user-123',
457+
},
458+
},
459+
});
460+
461+
transport.internalLogger = mockInternalLogger;
462+
463+
transport.send([logTransportItem]);
464+
465+
expect(fetch).toHaveBeenCalledTimes(1);
466+
expect(fetch).toHaveBeenCalledWith(
467+
'https://www.example.com/v1/logs',
468+
expect.objectContaining({
469+
headers: expect.objectContaining({
470+
Authorization: 'Bearer dynamic-token',
471+
'X-User': 'user-123',
472+
}),
473+
})
474+
);
475+
});
476+
477+
it('will add static header values and dynamic header values from sync callbacks', () => {
478+
const transport = new OtlpHttpTransport({
479+
logsURL: 'https://www.example.com/v1/logs',
480+
requestOptions: {
481+
headers: {
482+
Authorization: () => 'Bearer dynamic-token',
483+
'X-Static': 'static-value',
484+
},
485+
},
486+
});
487+
488+
transport.internalLogger = mockInternalLogger;
489+
490+
transport.send([logTransportItem]);
491+
492+
expect(fetch).toHaveBeenCalledTimes(1);
493+
expect(fetch).toHaveBeenCalledWith(
494+
'https://www.example.com/v1/logs',
495+
expect.objectContaining({
496+
headers: expect.objectContaining({
497+
Authorization: 'Bearer dynamic-token',
498+
'X-Static': 'static-value',
499+
}),
500+
})
501+
);
502+
});
503+
423504
it('will not send traces data if traces URL is not set', () => {
424505
const transport = new OtlpHttpTransport({
425506
logsURL: 'www.example.com/v1/logs',

experimental/transport-otlp-http/src/transport.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,23 @@ export class OtlpHttpTransport extends BaseTransport {
8989
const body = JSON.stringify({ [key]: value });
9090

9191
const { requestOptions, apiKey } = this.options;
92-
const { headers, ...restOfRequestOptions } = requestOptions ?? {};
92+
const { headers = {}, ...restOfRequestOptions } = requestOptions ?? {};
9393

9494
if (!url) {
9595
continue;
9696
}
9797

98+
const resolvedHeaders: Record<string, string> = {};
99+
for (const [key, value] of Object.entries(headers)) {
100+
resolvedHeaders[key] = typeof value === 'function' ? value() : value;
101+
}
102+
98103
this.promiseBuffer.add(() => {
99104
return fetch(url, {
100105
method: 'POST',
101106
headers: {
102107
'Content-Type': 'application/json',
103-
...(headers ?? {}),
108+
...resolvedHeaders,
104109
...(apiKey ? { 'x-api-key': apiKey } : {}),
105110
},
106111
body,

experimental/transport-otlp-http/src/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import type { ExceptionEvent, MeasurementEvent, TransportItem } from '@grafana/faro-core';
22

33
export interface OtlpTransportRequestOptions extends Omit<RequestInit, 'body' | 'headers'> {
4-
headers?: Record<string, string>;
4+
/**
5+
* Headers to include in every request.
6+
* Each value can be:
7+
* - a string (static value)
8+
* - a function returning a string (dynamic value)
9+
*/
10+
headers?: Record<string, string | (() => string)>;
511
}
612

713
export interface OtlpHttpTransportOptions {

packages/web-sdk/src/transports/fetch/transport.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,93 @@ describe('FetchTransport', () => {
249249
expect(ignoreUrls).toStrictEqual([collectorUrl, ...globalIgnoreUrls]);
250250
});
251251

252+
it('will add static header values', () => {
253+
const transport = new FetchTransport({
254+
url: 'http://example.com/collect',
255+
requestOptions: {
256+
headers: {
257+
Authorization: 'Bearer static-token',
258+
'X-Static': 'static-value',
259+
},
260+
},
261+
});
262+
263+
transport.metas.value = { session: { id: mockSessionId } };
264+
265+
transport.internalLogger = mockInternalLogger;
266+
267+
transport.send([item]);
268+
269+
expect(fetch).toHaveBeenCalledTimes(1);
270+
expect(fetch).toHaveBeenCalledWith(
271+
'http://example.com/collect',
272+
expect.objectContaining({
273+
headers: expect.objectContaining({
274+
Authorization: 'Bearer static-token',
275+
'X-Static': 'static-value',
276+
}),
277+
})
278+
);
279+
});
280+
281+
it('will add dynamic header values from sync callbacks', () => {
282+
const transport = new FetchTransport({
283+
url: 'http://example.com/collect',
284+
requestOptions: {
285+
headers: {
286+
Authorization: () => `Bearer ${mockSessionId}-token`,
287+
'X-User': () => 'user-123',
288+
},
289+
},
290+
});
291+
292+
transport.metas.value = { session: { id: mockSessionId } };
293+
294+
transport.internalLogger = mockInternalLogger;
295+
296+
transport.send([item]);
297+
298+
expect(fetch).toHaveBeenCalledTimes(1);
299+
expect(fetch).toHaveBeenCalledWith(
300+
'http://example.com/collect',
301+
expect.objectContaining({
302+
headers: expect.objectContaining({
303+
Authorization: `Bearer ${mockSessionId}-token`,
304+
'X-User': 'user-123',
305+
}),
306+
})
307+
);
308+
});
309+
310+
it('will add static header values and dynamic header values from sync callbacks', () => {
311+
const transport = new FetchTransport({
312+
url: 'http://example.com/collect',
313+
requestOptions: {
314+
headers: {
315+
Authorization: () => `Bearer ${mockSessionId}-token`,
316+
'X-Static': 'static-value',
317+
},
318+
},
319+
});
320+
321+
transport.metas.value = { session: { id: mockSessionId } };
322+
323+
transport.internalLogger = mockInternalLogger;
324+
325+
transport.send([item]);
326+
327+
expect(fetch).toHaveBeenCalledTimes(1);
328+
expect(fetch).toHaveBeenCalledWith(
329+
'http://example.com/collect',
330+
expect.objectContaining({
331+
headers: expect.objectContaining({
332+
Authorization: `Bearer ${mockSessionId}-token`,
333+
'X-Static': 'static-value',
334+
}),
335+
})
336+
);
337+
});
338+
252339
it('creates a new faro session if collector response indicates an invalid session', async () => {
253340
fetch.mockImplementationOnce(() =>
254341
Promise.resolve({

packages/web-sdk/src/transports/fetch/transport.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,24 @@ export class FetchTransport extends BaseTransport {
4949

5050
const { url, requestOptions, apiKey } = this.options;
5151

52-
const { headers, ...restOfRequestOptions } = requestOptions ?? {};
52+
const { headers = {}, ...restOfRequestOptions } = requestOptions ?? {};
5353

5454
let sessionId;
5555
const sessionMeta = this.metas.value.session;
5656
if (sessionMeta != null) {
5757
sessionId = sessionMeta.id;
5858
}
5959

60+
const resolvedHeaders: Record<string, string> = {};
61+
for (const [key, value] of Object.entries(headers)) {
62+
resolvedHeaders[key] = typeof value === 'function' ? value() : value;
63+
}
64+
6065
return fetch(url, {
6166
method: 'POST',
6267
headers: {
6368
'Content-Type': 'application/json',
64-
...(headers ?? {}),
69+
...resolvedHeaders,
6570
...(apiKey ? { 'x-api-key': apiKey } : {}),
6671
...(sessionId ? { 'x-faro-session-id': sessionId } : {}),
6772
},

packages/web-sdk/src/transports/fetch/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
export interface FetchTransportRequestOptions extends Omit<RequestInit, 'body' | 'headers'> {
2-
headers?: Record<string, string>;
2+
/**
3+
* Headers to include in every request.
4+
* Each value can be:
5+
* - a string (static value)
6+
* - a function returning a string (dynamic value)
7+
*/
8+
headers?: Record<string, string | (() => string)>;
39
}
410

511
export interface FetchTransportOptions {

0 commit comments

Comments
 (0)