DEV Community

loading...
Cover image for Client-Side Logging and Analytics with AWS

Client-Side Logging and Analytics with AWS

Rakan Nimer
Software Engineer @ Amazon Web Services 🐈 πŸ’» 🎢 πŸ“—
・4 min read

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

Architecture Overview

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

  • 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 !

Infrastructure Setup

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

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

// 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,
});
Enter fullscreen mode Exit fullscreen mode

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

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,
      },
    }
  );
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Usage

You can find how the telemetry class is used by this react app.

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;
Enter fullscreen mode Exit fullscreen mode

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

Discussion (2)

Collapse
arantespp profile image
Pedro Arantes

@rakannimer I can't access the client code on GitHub.

Collapse
rakannimer profile image
Rakan Nimer Author

Thanks for the heads up, fixed : github.com/rakannimer/client-side-...