diff --git a/app/custom_headers.html b/app/custom_headers.html new file mode 100644 index 00000000..7170c191 --- /dev/null +++ b/app/custom_headers.html @@ -0,0 +1,94 @@ + + + + RUM Integ Test + + + + + + + + +

This application is used for RUM integ testing.

+ + + +
+ + + +
+ + + + + + + + + + + + + + + +
Request URL
Request Header
Request Body
+ + + + + + + + + + + + + +
Response Status Code
Response Header
Response Body
+ sample + + diff --git a/docs/configuration.md b/docs/configuration.md index ab846212..ba141042 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,6 +43,7 @@ For example, the config object may look similar to the following: | dispatchInterval | Number | `5000` | The frequency (in milliseconds) in which the webclient will dispatch a batch of RUM events. RUM events are first cached and then automatically dispatched at this set interval. | | eventCacheSize | Number | `200` | The maximum number of events the cache can contain before dropping events. | | sessionLengthSeconds | Number | `1800` | The duration of a session (in seconds). | +| headers | Object | `{}` | The **headers** configuration is optional and allows you to include custom headers in an HTTP request. For example, you can use it to pass `Authorization` and `x-api-key` headers.

For more details, see: [MDN - Request Headers](https://developer.mozilla.org/en-US/docs/Glossary/Request_header). | ## CookieAttributes diff --git a/src/dispatch/DataPlaneClient.ts b/src/dispatch/DataPlaneClient.ts index d46eb597..64044675 100644 --- a/src/dispatch/DataPlaneClient.ts +++ b/src/dispatch/DataPlaneClient.ts @@ -4,7 +4,8 @@ import { AwsCredentialIdentityProvider, AwsCredentialIdentity, HttpResponse, - RequestPresigningArguments + RequestPresigningArguments, + HeaderBag } from '@aws-sdk/types'; import { Sha256 } from '@aws-crypto/sha256-js'; import { HttpHandler, HttpRequest } from '@aws-sdk/protocol-http'; @@ -47,6 +48,7 @@ export declare type DataPlaneClientConfig = { | AwsCredentialIdentityProvider | AwsCredentialIdentity | undefined; + headers?: HeaderBag; }; export class DataPlaneClient { @@ -118,7 +120,8 @@ export class DataPlaneClient { port: Number(this.config.endpoint.port) || undefined, headers: { 'content-type': contentType, - host: this.config.endpoint.host + host: this.config.endpoint.host, + ...this.config.headers }, hostname: this.config.endpoint.hostname, path: `${path}/appmonitors/${putRumEventsRequest.AppMonitorDetails.id}`, diff --git a/src/dispatch/Dispatch.ts b/src/dispatch/Dispatch.ts index 4c111c89..ffd74fa7 100644 --- a/src/dispatch/Dispatch.ts +++ b/src/dispatch/Dispatch.ts @@ -39,6 +39,7 @@ export class Dispatch { private buildClient: ClientBuilder; private config: Config; private disableCodes = ['403', '404']; + private headers: any; constructor( region: string, @@ -52,6 +53,7 @@ export class Dispatch { this.enabled = true; this.buildClient = config.clientBuilder || this.defaultClientBuilder; this.config = config; + this.headers = config.headers; this.startDispatchTimer(); if (config.signing) { this.rum = { @@ -265,7 +267,8 @@ export class Dispatch { beaconRequestHandler: new BeaconHttpHandler(), endpoint, region, - credentials + credentials, + headers: this.headers }); }; } diff --git a/src/dispatch/__tests__/DataPlaneClient.test.ts b/src/dispatch/__tests__/DataPlaneClient.test.ts index 0b4b4777..9409b05a 100644 --- a/src/dispatch/__tests__/DataPlaneClient.test.ts +++ b/src/dispatch/__tests__/DataPlaneClient.test.ts @@ -4,6 +4,7 @@ import { FetchHttpHandler } from '@aws-sdk/fetch-http-handler'; import { DataPlaneClient } from '../DataPlaneClient'; import { HttpRequest } from '@aws-sdk/protocol-http'; import { advanceTo } from 'jest-date-mock'; +import { HeaderBag } from '@aws-sdk/types'; const beaconHandler = jest.fn(() => Promise.resolve()); jest.mock('../BeaconHttpHandler', () => ({ @@ -21,6 +22,7 @@ jest.mock('@aws-sdk/fetch-http-handler', () => ({ interface Config { signing: boolean; endpoint: URL; + headers?: HeaderBag; } const defaultConfig = { signing: true, endpoint: Utils.AWS_RUM_ENDPOINT }; @@ -33,7 +35,8 @@ const createDataPlaneClient = ( beaconRequestHandler: new BeaconHttpHandler(), endpoint: config.endpoint, region: Utils.AWS_RUM_REGION, - credentials: config.signing ? Utils.createAwsCredentials() : undefined + credentials: config.signing ? Utils.createAwsCredentials() : undefined, + headers: config.headers ? config.headers : undefined }); }; @@ -345,4 +348,35 @@ describe('DataPlaneClient tests', () => { expect(signedRequest.query['X-Amz-SignedHeaders']).toEqual(undefined); expect(signedRequest.query['X-Amz-Signature']).toEqual(undefined); }); + + test('when the headers contains in config', async () => { + // Init + const endpoint = new URL(`${Utils.AWS_RUM_ENDPOINT}${'prod'}`); + const headers = { + Authorization: `Bearer token`, + 'x-api-key': 'a1b2c3d4e5f6', + 'content-type': 'application/json' + }; + const client: DataPlaneClient = createDataPlaneClient({ + ...defaultConfig, + endpoint, + headers, + signing: false + }); + + // Run + await client.sendFetch(Utils.PUT_RUM_EVENTS_REQUEST); + + // Assert + const signedRequest: HttpRequest = ( + fetchHandler.mock.calls[0] as any + )[0]; + console.log('signedRequest :>> ', signedRequest); + expect(signedRequest.headers['Authorization']).toEqual( + headers['Authorization'] + ); + expect(signedRequest.headers['x-api-key']).toEqual( + headers['x-api-key'] + ); + }); }); diff --git a/src/loader/loader-custom-headers.js b/src/loader/loader-custom-headers.js new file mode 100644 index 00000000..1b119a6f --- /dev/null +++ b/src/loader/loader-custom-headers.js @@ -0,0 +1,28 @@ +import { loader } from './loader'; +import { showRequestClientBuilder } from '../test-utils/mock-http-custom-headers'; + +const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; +loader( + 'cwr', + 'a1b2c3d4-c3f5-1a2b-b2b4-012345678910', + '1.0', + 'us-west-2', + './rum_javascript_telemetry.js', + { + userIdRetentionDays: 1, + dispatchInterval: 0, + allowCookies: false, + eventPluginsToLoad: [], + telemetries: [], + clientBuilder: showRequestClientBuilder, + signing: false, + endpoint: + 'https://api-id.execute-api.region.amazonaws.com/api/dataplane', + headers: { + Authorization: `Bearer ${token}`, + 'x-api-key': 'a1b2c3d4e5f6', + 'content-type': 'application/json' + } + } +); diff --git a/src/orchestration/Orchestration.ts b/src/orchestration/Orchestration.ts index 0e593052..df079423 100644 --- a/src/orchestration/Orchestration.ts +++ b/src/orchestration/Orchestration.ts @@ -17,7 +17,8 @@ import { EventCache } from '../event-cache/EventCache'; import { ClientBuilder, Dispatch } from '../dispatch/Dispatch'; import { AwsCredentialIdentityProvider, - AwsCredentialIdentity + AwsCredentialIdentity, + HeaderBag } from '@aws-sdk/types'; import { NavigationPlugin } from '../plugins/event-plugins/NavigationPlugin'; import { ResourcePlugin } from '../plugins/event-plugins/ResourcePlugin'; @@ -160,6 +161,7 @@ export interface Config { useBeacon: boolean; userIdRetentionDays: number; alias?: string; + headers?: HeaderBag; } export interface PartialConfig diff --git a/src/test-utils/mock-http-custom-headers.ts b/src/test-utils/mock-http-custom-headers.ts new file mode 100644 index 00000000..93f13d2d --- /dev/null +++ b/src/test-utils/mock-http-custom-headers.ts @@ -0,0 +1,48 @@ +import { HttpHandler, HttpRequest, HttpResponse } from '@aws-sdk/protocol-http'; +import { ClientBuilder } from '../dispatch/Dispatch'; +import { DataPlaneClient } from '../dispatch/DataPlaneClient'; +import { logRequestToPage, logResponseToPage } from './http-handler-utils'; + +/** + * Returns data plane service client with a mocked request handler. + * + * @param endpoint Service endpoint. + * @param region Service region. + * @param credentials AWS credentials. + */ +const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; +export const showRequestClientBuilder: ClientBuilder = ( + endpoint, + region, + credentials +) => { + return new DataPlaneClient({ + fetchRequestHandler: new ShowMockRequestHandler(), + beaconRequestHandler: new ShowMockRequestHandler(), + endpoint, + region, + credentials, + headers: { + Authorization: `Bearer ${token}`, + 'x-api-key': 'a1b2c3d4e5f6', + 'content-type': 'application/json' + } + }); +}; + +class ShowMockRequestHandler implements HttpHandler { + handle(request: HttpRequest): Promise<{ response: HttpResponse }> { + const response: HttpResponse = { + statusCode: 202, + headers: { + 'content-type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: undefined + }; + logRequestToPage(request); + logResponseToPage(response); + return Promise.resolve({ response }); + } +} diff --git a/webpack/webpack.dev.js b/webpack/webpack.dev.js index 8e9562d4..ef698f24 100644 --- a/webpack/webpack.dev.js +++ b/webpack/webpack.dev.js @@ -31,7 +31,8 @@ module.exports = merge(common, { loader_remote_config: './src/loader/loader-remote-config.js', loader_spa: './src/loader/loader-spa.js', loader_custom_events: './src/loader/loader-custom-events.js', - loader_alias: './src/loader/loader-alias.js' + loader_alias: './src/loader/loader-alias.js', + loader_custom_headers: './src/loader/loader-custom-headers.js' }, resolve: { extensions: ['.ts', '.js', '.json']