Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Next

- 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).

- 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).

## 2.0.2

- Breaking (`@grafana/faro-web-sdk`): User action events now have a standardized event name
Expand Down
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Next

- Added support for experimental `ReplayInstrumentation` (`@grafana/faro-instrumentation-replay`).
- 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).

## 1.13.0

Expand Down
81 changes: 81 additions & 0 deletions experimental/transport-otlp-http/src/transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,87 @@ describe('OtlpHttpTransport', () => {
expect(mockResponseTextFn).toHaveBeenCalledTimes(1);
});

it('will add static header values', () => {
const transport = new OtlpHttpTransport({
logsURL: 'https://www.example.com/v1/logs',
requestOptions: {
headers: {
Authorization: 'Bearer static-token',
'X-Static': 'static-value',
},
},
});

transport.internalLogger = mockInternalLogger;

transport.send([logTransportItem]);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
'https://www.example.com/v1/logs',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer static-token',
'X-Static': 'static-value',
}),
})
);
});

it('will add dynamic header values from sync callbacks', () => {
const transport = new OtlpHttpTransport({
logsURL: 'https://www.example.com/v1/logs',
requestOptions: {
headers: {
Authorization: () => 'Bearer dynamic-token',
'X-User': () => 'user-123',
},
},
});

transport.internalLogger = mockInternalLogger;

transport.send([logTransportItem]);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
'https://www.example.com/v1/logs',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer dynamic-token',
'X-User': 'user-123',
}),
})
);
});

it('will add static header values and dynamic header values from sync callbacks', () => {
const transport = new OtlpHttpTransport({
logsURL: 'https://www.example.com/v1/logs',
requestOptions: {
headers: {
Authorization: () => 'Bearer dynamic-token',
'X-Static': 'static-value',
},
},
});

transport.internalLogger = mockInternalLogger;

transport.send([logTransportItem]);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
'https://www.example.com/v1/logs',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer dynamic-token',
'X-Static': 'static-value',
}),
})
);
});

it('will not send traces data if traces URL is not set', () => {
const transport = new OtlpHttpTransport({
logsURL: 'www.example.com/v1/logs',
Expand Down
9 changes: 7 additions & 2 deletions experimental/transport-otlp-http/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,23 @@ export class OtlpHttpTransport extends BaseTransport {
const body = JSON.stringify({ [key]: value });

const { requestOptions, apiKey } = this.options;
const { headers, ...restOfRequestOptions } = requestOptions ?? {};
const { headers = {}, ...restOfRequestOptions } = requestOptions ?? {};

if (!url) {
continue;
}

const resolvedHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
resolvedHeaders[key] = typeof value === 'function' ? value() : value;
}

this.promiseBuffer.add(() => {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(headers ?? {}),
...resolvedHeaders,
...(apiKey ? { 'x-api-key': apiKey } : {}),
},
body,
Expand Down
8 changes: 7 additions & 1 deletion experimental/transport-otlp-http/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { ExceptionEvent, MeasurementEvent, TransportItem } from '@grafana/faro-core';

export interface OtlpTransportRequestOptions extends Omit<RequestInit, 'body' | 'headers'> {
headers?: Record<string, string>;
/**
* Headers to include in every request.
* Each value can be:
* - a string (static value)
* - a function returning a string (dynamic value)
*/
headers?: Record<string, string | (() => string)>;
}

export interface OtlpHttpTransportOptions {
Expand Down
87 changes: 87 additions & 0 deletions packages/web-sdk/src/transports/fetch/transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,93 @@ describe('FetchTransport', () => {
expect(ignoreUrls).toStrictEqual([collectorUrl, ...globalIgnoreUrls]);
});

it('will add static header values', () => {
const transport = new FetchTransport({
url: 'http://example.com/collect',
requestOptions: {
headers: {
Authorization: 'Bearer static-token',
'X-Static': 'static-value',
},
},
});

transport.metas.value = { session: { id: mockSessionId } };

transport.internalLogger = mockInternalLogger;

transport.send([item]);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
'http://example.com/collect',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer static-token',
'X-Static': 'static-value',
}),
})
);
});

it('will add dynamic header values from sync callbacks', () => {
const transport = new FetchTransport({
url: 'http://example.com/collect',
requestOptions: {
headers: {
Authorization: () => `Bearer ${mockSessionId}-token`,
'X-User': () => 'user-123',
},
},
});

transport.metas.value = { session: { id: mockSessionId } };

transport.internalLogger = mockInternalLogger;

transport.send([item]);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
'http://example.com/collect',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockSessionId}-token`,
'X-User': 'user-123',
}),
})
);
});

it('will add static header values and dynamic header values from sync callbacks', () => {
const transport = new FetchTransport({
url: 'http://example.com/collect',
requestOptions: {
headers: {
Authorization: () => `Bearer ${mockSessionId}-token`,
'X-Static': 'static-value',
},
},
});

transport.metas.value = { session: { id: mockSessionId } };

transport.internalLogger = mockInternalLogger;

transport.send([item]);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
'http://example.com/collect',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockSessionId}-token`,
'X-Static': 'static-value',
}),
})
);
});

it('creates a new faro session if collector response indicates an invalid session', async () => {
fetch.mockImplementationOnce(() =>
Promise.resolve({
Expand Down
9 changes: 7 additions & 2 deletions packages/web-sdk/src/transports/fetch/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,24 @@ export class FetchTransport extends BaseTransport {

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

const { headers, ...restOfRequestOptions } = requestOptions ?? {};
const { headers = {}, ...restOfRequestOptions } = requestOptions ?? {};

let sessionId;
const sessionMeta = this.metas.value.session;
if (sessionMeta != null) {
sessionId = sessionMeta.id;
}

const resolvedHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
resolvedHeaders[key] = typeof value === 'function' ? value() : value;
}

return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(headers ?? {}),
...resolvedHeaders,
...(apiKey ? { 'x-api-key': apiKey } : {}),
...(sessionId ? { 'x-faro-session-id': sessionId } : {}),
},
Expand Down
8 changes: 7 additions & 1 deletion packages/web-sdk/src/transports/fetch/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export interface FetchTransportRequestOptions extends Omit<RequestInit, 'body' | 'headers'> {
headers?: Record<string, string>;
/**
* Headers to include in every request.
* Each value can be:
* - a string (static value)
* - a function returning a string (dynamic value)
*/
headers?: Record<string, string | (() => string)>;
}

export interface FetchTransportOptions {
Expand Down