Client-Side Logging and Analytics with AWS
Tags: aws, cdk, javascript
Published: 2021-06-06 23:22:47

---
title: Client-Side Logging and Analytics with AWS
published: true
description: Setup telemetry for your apps with AWS
tags: aws, cdk, javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hunt5d7x4ayz2lh67a1e.png
---

This post goes through an example setup of client-side log and analytics collection from authenticated and guest clients, using AWS services.

The work will be split into two parts :

- Infrastructure Setup : Creating the required infrastructure using the AWS CDK
- Client-Side Integration : Interacting with AWS APIs from the client

The following AWS services will be used

- [Amazon Cognito - Authentication](https://aws.amazon.com/cognito/)
- [Amazon CloudWatch - Application and Infrastructure Monitoring](https://aws.amazon.com/cloudwatch/)
- [Amazon Pinpoint - Customer Engagement](https://aws.amazon.com/pinpoint/)

![Architecture Overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46zr76j8h8os8yviyb98.png)

The client will retrieve temporary AWS credentials using Amazon Cognito, and use these credentials to log events to CloudWatch and Pinpoint.


Notes :

- If you are using / can use Amplify, you don't need any of this, the nice folks there have got you covered : just add Auth and Analytics categories and you're good to go. [Amplify Docs](https://docs.amplify.aws/start/q/integration/js)

- This post is just a wrap-up of my experience playing with these services for my future recollection. Please do not treat this as official advice in any way.

> Just show me the code, please !

[Here you go !](https://github.com/rakannimer/client-side-aws-telemetry)

## Infrastructure Setup

This solution doesn't add much infrastructure to maintain, here's what we need :

- A [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html) (with unauthenticated guest access)
- A [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html)
- An IAM Role for authenticated users
- An IAM Role for un-authenticated users

For this post, a similar IAM role will be provided for both.

The IAM role will be granted to all visitors so the permissions that will be granted need to be as minimal as possible.

The following permissions will be given :

- `logs:CreateLogStream` - Each user needs to create their own log stream. The log group is created by the admin account.
- `logs:PutLogEvents` - Allows user to send logs to cloudwatch
- `mobiletargeting:PutEvents` - Allows user to send events to Amazon Pinpoint

This can be done using the AWS console but let's use the CDK to commit our infrastructure as code.

Example TypeScript code can be found [here](https://github.com/rakannimer/client-side-aws-telemetry/tree/master/client-side-telemetry-cdk)

```ts
// Create resources
const userPool = new cognito.UserPool(this, "user-pool", {});

const userPoolClient = new cognito.UserPoolClient(this, "UserPoolClient", {
  userPool,
  generateSecret: false, // Don't need to generate secret for web app running on browsers
});

const identityPool = new cognito.CfnIdentityPool(this, "IdentityPool", {
  allowUnauthenticatedIdentities: true, // Allow unathenticated users
  cognitoIdentityProviders: [
    {
      clientId: userPoolClient.userPoolClientId,
      providerName: userPool.userPoolProviderName,
    },
  ],
});

const pinpointApp = new pinpoint.CfnApp(this, "PinpointApp", {
  name: `pinpoint-${identityPool.ref}`,
});
// In next code block
createCognitoIamRoles(this, identityPool.ref);

// Export values
new CfnOutput(this, "PinPointAppId", {
  value: pinpointApp.ref,
});
new CfnOutput(this, "UserPoolId", {
  value: userPool.userPoolId,
});
new CfnOutput(this, "UserPoolClientId", {
  value: userPoolClient.userPoolClientId,
});
new CfnOutput(this, "IdentityPoolId", {
  value: identityPool.ref,
});
```

This sets up all the resources except creating the needed IAM Roles and attaching them to the existing identity pool

```ts
import * as cdk from "@aws-cdk/core";
import * as iam from "@aws-cdk/aws-iam";
import * as cognito from "@aws-cdk/aws-cognito";

const cloudwatchPermissionPolicy = new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ["logs:PutLogEvents", "logs:CreateLogStream"],
  resources: ["arn:aws:logs:*:*:log-group:*:log-stream:*"],
});

const pinpointPutEventsPolicy = new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ["mobiletargeting:PutEvents", "mobiletargeting:UpdateEndpoint"],
  resources: ["arn:aws:mobiletargeting:*:*:apps/*"],
});

const getRole = (identityPoolRef: string, authed: boolean) => ({
  assumedBy: new iam.FederatedPrincipal(
    "cognito-identity.amazonaws.com",
    {
      StringEquals: {
        "cognito-identity.amazonaws.com:aud": identityPoolRef,
      },
      "ForAnyValue:StringLike": {
        "cognito-identity.amazonaws.com:amr": authed
          ? "authenticated"
          : "unauthenticated",
      },
    },
    "sts:AssumeRoleWithWebIdentity"
  ),
});

export const createCognitoIamRoles = (
  scope: cdk.Construct,
  identityPoolRef: string
) => {
  const authedRole = new iam.Role(
    scope,
    "CognitoAuthenticatedRole",
    getRole(identityPoolRef, true)
  );
  const unAuthedRole = new iam.Role(
    scope,
    "CognitoUnAuthenticatedRole",
    getRole(identityPoolRef, false)
  );
  authedRole.addToPolicy(cloudwatchPermissionPolicy);
  authedRole.addToPolicy(pinpointPutEventsPolicy);

  unAuthedRole.addToPolicy(cloudwatchPermissionPolicy);
  unAuthedRole.addToPolicy(pinpointPutEventsPolicy);

  new cognito.CfnIdentityPoolRoleAttachment(
    scope,
    "IdentityPoolRoleAttachment",
    {
      identityPoolId: identityPoolRef,
      roles: {
        authenticated: authedRole.roleArn,
        unauthenticated: unAuthedRole.roleArn,
      },
    }
  );
};
```

To create the resources, run `npm run deploy` in the CDK repository. This will generate the needed resources and output some variable that will be needed in the next section.

Example output:

```
ClientSideLogTestCdkStack.IdentityPoolId = us-east-1:bc36bea5-5b0f-486a-8812-c68c2a5e4842
ClientSideLogTestCdkStack.PinPointAppId = a915587bb416449a8407fdd75bd6a0fe
ClientSideLogTestCdkStack.UserPoolClientId = 2sjihthbvodq1pos6m29mi6c2j
ClientSideLogTestCdkStack.UserPoolId = us-east-1_z4PrZ5N3Z
```

## Client-Side Integration

Now that the needed infrastructure is ready we can start writing client code to interact with it.

To do that, let's create a `Telemetry` class ( or whatever you would like to call it ) and use that as our entry-point to the provisioned AWS infrastructure.

This class should:

- Give access to Amplify's `Analytics` and `Auth` libraries

The Amplify team have done the heavy lifting to provide user-friendly APIs, this implementation should attempt to leverage that work.

- Offer a simple abstraction over the CloudWatch client-logs API

The client doing the logging shouldn't care about CloudWatch APIs to be able to send logs. This telemetry client implementation provides three logging methods (`info`, `warn` and `error`)

On instantiation, the object : - Retrieves credentials from Cognito - Creates a cloudwatch client - Instantiates Amplify's Auth and Analytics - Sets up a recurring timer to send collected logs to cloudwatch every 2 seconds.

You can find an example implementation [`here`](https://github.com/rakannimer/client-side-aws-telemetry/blob/master/client-side-telemetry-js/index.js)

## Usage

You can find how the telemetry class is used by [this react app](https://github.com/rakannimer/client-side-aws-telemetry/tree/master/client-side-telemetry-js-example).

```tsx
import React from "react";
// client-side-telemetry-js = https://github.com/rakannimer/client-side-aws-telemetry/blob/master/client-side-telemetry-js/index.js
import AwsTelemetry from "client-side-telemetry-js";

// Config values are logged after you finish deployment with the CDK
const telemetryConfig = {
  identityPoolId: "us-east-1:xxxxx-5b0f-486a-yzyz-c68c2a5ea2z2",
  userPoolWebClientId: "2sjihyyyyyyypos6m29mi6c2j",
  userPoolId: "us-east-1_z4PrZ5N3Z",
  region: "us-east-1",
  pinpointAppId: "d9ad53bad9d1qwe1w93d7de2499c7gf5",
};

const logger = new AwsTelemetry(telemetryConfig);

function App() {
  React.useEffect(() => {
    logger.info(`Hello`);
    setTimeout(() => {
      logger.info(`Hello 2 seconds later`);
    }, 2200);
  }, []);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <button
          onClick={() => {
            logger.warn("User clicked a button");
          }}
        >
          Send a message to cloudwatch
        </button>
      </header>
    </div>
  );
}

export default App;
```


## End Result

This should enable you to collect logs from your clients to identify and resolve problems before your customers have to report them.

Bonus Amazon Pinpoint Dashboard Screenshot : 

![Amazon Pinpoint Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0p57j7nbf9tzurczvgz1.png)