Skip to content

Commit

Permalink
feat: Use data plane endpoint path prefix. (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
qhanam committed May 18, 2022
1 parent 69aa5a0 commit 3dd112f
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 69 deletions.
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ For example, the config object may look similar to the following:
| disableAutoPageView | Boolean | `false` | When this field is `false`, the web client will automatically record page views.<br/><br/>By default, the web client records page views when (1) the page first loads and (2) the browser's [history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) is called. The page ID is `window.location.pathname`.<br/><br/>In some cases, the web client's instrumentation will not record the desired page ID. In this case, the web client's page view automation must be disabled using the `disableAutoPageView` configuration, and the application must be instrumented to record page views using the `recordPageView` command. |
| enableRumClient | Boolean | `true` | When this field is `true`, the web client will record and dispatch RUM events. |
| enableXRay | Boolean | `false` | When this field is `true` **and** the `http` telemetry is used, the web client will record X-Ray traces for HTTP requests.<br/><br/>See the [HTTP telemetry configuration](#http) for more information, including how to connect client-side and server-side traces. |
| endpoint | String | `'https://dataplane.rum.[region].amazonaws.com'` | The URL of the CloudWatch RUM API where data will be sent. |
| endpoint | String | `'https://dataplane.rum.[region].amazonaws.com'`<br/><br/>`'https://[restapi_id].execute-api.[region].amazonaws.com/[stage_name]/'` | The URL of the CloudWatch RUM API where data will be sent.<br/><br/>You may include a path prefix like `/stage_name/` in the endpoint URL if there is a proxy between your web application and CloudWatch RUM. |
| guestRoleArn | String | `undefined` | The ARN of the AWS IAM role that will be assumed during anonymous authorization.<br/><br/>When this field is set (along with `identityPoolId`), the web client will attempt to retrieve temporary AWS credentials through Cognito using `AssumeRoleWithWebIdentity`. If this field is not set, you must forward credentials to the web client using the `setAwsCredentials` command. |
| identityPoolId | String | `undefined` | The Amazon Cognito Identity Pool ID that will be used during anonymous authorization.<br/><br/>When this field is set (along with `guestRoleArn`), the web client will attempt to retrieve temporary AWS credentials through Cognito using `AssumeRoleWithWebIdentity`. If this field is not set, you must forward credentials to the web client using the `setAwsCredentials` command. |
| pageIdFormat | String | `'PATH'` | The portion of the `window.location` that will be used as the page ID. Options include `PATH`, `HASH` and `PATH_AND_HASH`.<br/><br/>For example, consider the URL `https://amazonaws.com/home?param=true#content`<br/><br/>`PATH`: `/home`<br/>`HASH`: `#content`<br/>`PATH_AND_HASH`: `/home#content` |
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 11 additions & 12 deletions src/dispatch/DataPlaneClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from '@aws-sdk/types';
import { Sha256 } from '@aws-crypto/sha256-js';
import { HttpHandler, HttpRequest } from '@aws-sdk/protocol-http';
import { getHost, getScheme } from '../utils/common-utils';
import {
AppMonitorDetails,
PutRumEventsRequest,
Expand Down Expand Up @@ -41,7 +40,7 @@ declare type SerializedPutRumEventsRequest = {
export declare type DataPlaneClientConfig = {
fetchRequestHandler: HttpHandler;
beaconRequestHandler: HttpHandler;
endpoint: string;
endpoint: URL;
region: string;
credentials: CredentialProvider | Credentials;
};
Expand All @@ -65,20 +64,20 @@ export class DataPlaneClient {
public sendFetch = async (
putRumEventsRequest: PutRumEventsRequest
): Promise<{ response: HttpResponse }> => {
const host = getHost(this.config.endpoint);
const serializedRequest: string = JSON.stringify(
serializeRequest(putRumEventsRequest)
);
const path = this.config.endpoint.pathname.replace(/\/$/, '');
const request = new HttpRequest({
method: METHOD,
headers: {
'content-type': CONTENT_TYPE_JSON,
'X-Amz-Content-Sha256': await hashAndEncode(serializedRequest),
host
host: this.config.endpoint.host
},
protocol: getScheme(this.config.endpoint),
hostname: host,
path: `/appmonitors/${putRumEventsRequest.AppMonitorDetails.id}/`,
protocol: this.config.endpoint.protocol,
hostname: this.config.endpoint.hostname,
path: `${path}/appmonitors/${putRumEventsRequest.AppMonitorDetails.id}/`,
body: serializedRequest
});

Expand All @@ -94,20 +93,20 @@ export class DataPlaneClient {
public sendBeacon = async (
putRumEventsRequest: PutRumEventsRequest
): Promise<{ response: HttpResponse }> => {
const host = getHost(this.config.endpoint);
const serializedRequest: string = JSON.stringify(
serializeRequest(putRumEventsRequest)
);
const path = this.config.endpoint.pathname.replace(/\/$/, '');
const request = new HttpRequest({
method: METHOD,
headers: {
'content-type': CONTENT_TYPE_TEXT,
'X-Amz-Content-Sha256': await hashAndEncode(serializedRequest),
host
host: this.config.endpoint.host
},
protocol: getScheme(this.config.endpoint),
hostname: host,
path: `/appmonitors/${putRumEventsRequest.AppMonitorDetails.id}`,
protocol: this.config.endpoint.protocol,
hostname: this.config.endpoint.hostname,
path: `${path}/appmonitors/${putRumEventsRequest.AppMonitorDetails.id}`,
body: serializedRequest
});

Expand Down
6 changes: 3 additions & 3 deletions src/dispatch/Dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ interface DataPlaneClientInterface {
const NO_CRED_MSG = 'CWR: Cannot dispatch; no AWS credentials.';

export type ClientBuilder = (
endpoint: string,
endpoint: URL,
region: string,
credentials: CredentialProvider | Credentials
) => DataPlaneClient;

export class Dispatch {
private region: string;
private endpoint: string;
private endpoint: URL;
private eventCache: EventCache;
private rum: DataPlaneClientInterface;
private enabled: boolean;
Expand All @@ -40,7 +40,7 @@ export class Dispatch {

constructor(
region: string,
endpoint: string,
endpoint: URL,
eventCache: EventCache,
config: Config
) {
Expand Down
99 changes: 97 additions & 2 deletions src/dispatch/__tests__/DataPlaneClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ describe('DataPlaneClient tests', () => {
await client.sendFetch(Utils.PUT_RUM_EVENTS_REQUEST);

// Assert
// @ts-ignore
const signedRequest: HttpRequest = fetchHandler.mock.calls[0][0];
const signedRequest: HttpRequest = (fetchHandler.mock
.calls[0] as any)[0];
expect(signedRequest.headers['x-amz-date']).toEqual('19700101T000000Z');
expect(signedRequest.headers['X-Amz-Content-Sha256']).toEqual(
'57bbd361f5c5ab66d7dafb33d6c8bf714bbb140300fad06145b8d66c388b5d43'
Expand Down Expand Up @@ -132,4 +132,99 @@ describe('DataPlaneClient tests', () => {
'd37eb756444ebf6f785233714d6d942f2b20f69292fb09533f6b69556eb0ff2b'
);
});

test('when the endpoint contains a path then the fetch request url contains the path prefix', async () => {
// Init
const endpoint = new URL(`${Utils.AWS_RUM_ENDPOINT}${'prod'}`);
const client: DataPlaneClient = new DataPlaneClient({
fetchRequestHandler: new FetchHttpHandler(),
beaconRequestHandler: new BeaconHttpHandler(),
endpoint,
region: Utils.AWS_RUM_REGION,
credentials: Utils.createAwsCredentials()
});

// Run
await client.sendFetch(Utils.PUT_RUM_EVENTS_REQUEST);

// Assert
const signedRequest: HttpRequest = (fetchHandler.mock
.calls[0] as any)[0];
expect(signedRequest.hostname).toEqual(Utils.AWS_RUM_ENDPOINT.hostname);
expect(signedRequest.path).toEqual(
`${endpoint.pathname}/appmonitors/application123/`
);
});

test('when the endpoint path contains a trailing slash then the fetch request url drops the trailing slash', async () => {
// Init
const endpoint = new URL(`${Utils.AWS_RUM_ENDPOINT}${'prod/'}`);
const client: DataPlaneClient = new DataPlaneClient({
fetchRequestHandler: new FetchHttpHandler(),
beaconRequestHandler: new BeaconHttpHandler(),
endpoint,
region: Utils.AWS_RUM_REGION,
credentials: Utils.createAwsCredentials()
});

// Run
await client.sendFetch(Utils.PUT_RUM_EVENTS_REQUEST);

// Assert
const signedRequest: HttpRequest = (fetchHandler.mock
.calls[0] as any)[0];
expect(signedRequest.hostname).toEqual(Utils.AWS_RUM_ENDPOINT.hostname);
expect(signedRequest.path).toEqual(
`${endpoint.pathname.replace(
/\/$/,
''
)}/appmonitors/application123/`
);
});

test('when the endpoint contains a path then the beacon request url contains the path prefix', async () => {
// Init
const endpoint = new URL(`${Utils.AWS_RUM_ENDPOINT}${'prod'}`);
const client: DataPlaneClient = new DataPlaneClient({
fetchRequestHandler: new FetchHttpHandler(),
beaconRequestHandler: new BeaconHttpHandler(),
endpoint,
region: Utils.AWS_RUM_REGION,
credentials: Utils.createAwsCredentials()
});

// Run
await client.sendBeacon(Utils.PUT_RUM_EVENTS_REQUEST);

// Assert
const signedRequest: HttpRequest = (beaconHandler.mock
.calls[0] as any)[0];
expect(signedRequest.hostname).toEqual(Utils.AWS_RUM_ENDPOINT.hostname);
expect(signedRequest.path).toEqual(
`${endpoint.pathname}/appmonitors/application123`
);
});

test('when the endpoint path contains a trailing slash then the beacon request url drops the trailing slash', async () => {
// Init
const endpoint = new URL(`${Utils.AWS_RUM_ENDPOINT}${'prod/'}`);
const client: DataPlaneClient = new DataPlaneClient({
fetchRequestHandler: new FetchHttpHandler(),
beaconRequestHandler: new BeaconHttpHandler(),
endpoint,
region: Utils.AWS_RUM_REGION,
credentials: Utils.createAwsCredentials()
});

// Run
await client.sendBeacon(Utils.PUT_RUM_EVENTS_REQUEST);

// Assert
const signedRequest: HttpRequest = (beaconHandler.mock
.calls[0] as any)[0];
expect(signedRequest.hostname).toEqual(Utils.AWS_RUM_ENDPOINT.hostname);
expect(signedRequest.path).toEqual(
`${endpoint.pathname.replace(/\/$/, '')}/appmonitors/application123`
);
});
});
46 changes: 35 additions & 11 deletions src/orchestration/Orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ import { FetchPlugin } from '../plugins/event-plugins/FetchPlugin';
import { PageViewPlugin } from '../plugins/event-plugins/PageViewPlugin';
import { PageAttributes } from '../sessions/PageManager';

const DATA_PLANE_REGION_PLACEHOLDER = '${REGION}';
const DATA_PLANE_DEFAULT_ENDPOINT =
'https://dataplane.rum.${REGION}.amazonaws.com';
const DEFAULT_REGION = 'us-west-2';
const DEFAULT_ENDPOINT = `https://dataplane.rum.${DEFAULT_REGION}.amazonaws.com`;

export enum TELEMETRY_TYPES {
ERRORS = 'errors',
Expand All @@ -47,6 +46,8 @@ export enum PAGE_ID_FORMAT {
PATH_AND_HASH = 'PATH_AND_HASH'
}

export type PageIdFormat = 'PATH' | 'HASH' | 'PATH_AND_HASH';

export type PartialCookieAttributes = {
unique?: boolean;
domain?: string;
Expand All @@ -69,7 +70,7 @@ export type PartialConfig = {
eventPluginsToLoad?: Plugin[];
guestRoleArn?: string;
identityPoolId?: string;
pageIdFormat?: PAGE_ID_FORMAT;
pageIdFormat?: PageIdFormat;
pagesToExclude?: RegExp[];
pagesToInclude?: RegExp[];
recordResourceUrl?: boolean;
Expand Down Expand Up @@ -113,7 +114,8 @@ export const defaultConfig = (cookieAttributes: CookieAttributes): Config => {
dispatchInterval: 5 * 1000,
enableRumClient: true,
enableXRay: false,
endpoint: 'https://dataplane.rum.us-west-2.amazonaws.com',
endpoint: DEFAULT_ENDPOINT,
endpointUrl: new URL(DEFAULT_ENDPOINT),
eventCacheSize: 200,
eventPluginsToLoad: [],
pageIdFormat: PAGE_ID_FORMAT.PATH,
Expand Down Expand Up @@ -148,6 +150,7 @@ export type Config = {
enableRumClient: boolean;
enableXRay: boolean;
endpoint: string;
endpointUrl: URL;
eventCacheSize: number;
eventPluginsToLoad: Plugin[];
/*
Expand All @@ -161,7 +164,7 @@ export type Config = {
) => Promise<Response>;
guestRoleArn?: string;
identityPoolId?: string;
pageIdFormat: PAGE_ID_FORMAT;
pageIdFormat: PageIdFormat;
pagesToExclude: RegExp[];
pagesToInclude: RegExp[];
recordResourceUrl: boolean;
Expand All @@ -187,6 +190,23 @@ export class Orchestration {
private dispatchManager: Dispatch;
private config: Config;

/**
* Instantiate the CloudWatch RUM web client and begin monitoring the
* application.
*
* This constructor may throw a TypeError if not correctly configured. In
* production code, wrap calls to this constructor in a try/catch block so
* that this does not impact the application.
*
* @param applicationId A globally unique identifier for the CloudWatch RUM
* app monitor which monitors your application.
* @param applicationVersion Your application's semantic version. If you do
* not wish to use this field then add any placeholder, such as '0.0.0'.
* @param region The AWS region of the app monitor. For example, 'us-east-1'
* or 'eu-west-2'.
* @param partialConfig An application-specific configuration for the web
* client.
*/
constructor(
applicationId: string,
applicationVersion: string,
Expand All @@ -213,6 +233,13 @@ export class Orchestration {

this.config.endpoint = this.getDataPlaneEndpoint(region, partialConfig);

// If the URL is not formatted correctly, a TypeError will be thrown.
// This breaks our convention to fail-safe here for the sake of
// debugging. It is expected that the application has wrapped the call
// to the constructor in a try/catch block, as is done in the example
// code.
this.config.endpointUrl = new URL(this.config.endpoint);

this.eventCache = this.initEventCache(
applicationId,
applicationVersion
Expand Down Expand Up @@ -331,7 +358,7 @@ export class Orchestration {
private initDispatch(region: string) {
const dispatch: Dispatch = new Dispatch(
region,
this.config.endpoint,
this.config.endpointUrl,
this.eventCache,
this.config
);
Expand Down Expand Up @@ -415,10 +442,7 @@ export class Orchestration {
): string {
return partialConfig.endpoint
? partialConfig.endpoint
: DATA_PLANE_DEFAULT_ENDPOINT.replace(
DATA_PLANE_REGION_PLACEHOLDER,
region
);
: DEFAULT_ENDPOINT.replace(DEFAULT_REGION, region);
}

/**
Expand Down
7 changes: 5 additions & 2 deletions src/orchestration/__tests__/Orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Orchestration tests', () => {
// Assert
expect(Dispatch).toHaveBeenCalledTimes(1);
expect((Dispatch as any).mock.calls[0][1]).toEqual(
'https://dataplane.rum.us-west-2.amazonaws.com'
new URL('https://dataplane.rum.us-west-2.amazonaws.com/')
);
});

Expand All @@ -67,7 +67,7 @@ describe('Orchestration tests', () => {
// Assert
expect(Dispatch).toHaveBeenCalledTimes(1);
expect((Dispatch as any).mock.calls[0][1]).toEqual(
'https://dataplane.rum.us-east-1.amazonaws.com'
new URL('https://dataplane.rum.us-east-1.amazonaws.com/')
);
});

Expand Down Expand Up @@ -148,6 +148,9 @@ describe('Orchestration tests', () => {
dispatchInterval: 5000,
enableXRay: false,
endpoint: 'https://dataplane.rum.us-west-2.amazonaws.com',
endpointUrl: new URL(
'https://dataplane.rum.us-west-2.amazonaws.com'
),
eventCacheSize: 200,
eventPluginsToLoad: [],
pageIdFormat: 'PATH',
Expand Down

0 comments on commit 3dd112f

Please sign in to comment.