Google Analytics Reporting Integration – Part 5 of 5: Salesforce Configuration

We are going to have to use Apex to integrate with Google Analytics. It provides us with the flexibility to do what is needed to construct the JSON and send and receive information to Google. Additionally, the Apex Class can be called in numerous ways, depending on the need and requirements of the implementation.

The first thing we need to do in Salesforce is create the Custom Field on the Lead that represents the unique customer identifier. We can use Text as the field type.

Picture14

The following fields can be configured as follows:

  • Field Label – Can be Visitor ID
  • Field Name – Leave this with the generated name, unless you have a deep desire to change it
  • Complete the Description and Help Text as you desire
  • Length – Very much depends on how you create your unique visitor ID within your Web Site. For my purposes 100 characters will be sufficient.

The Custom Field need not appear on any page layouts unless you really want to see what’s going on, but it MUST be completed in the Web-to-Lead form, so make sure that this has been captured. Again, this is not something you need to display to the user of the Web Site, something like this in your HTML will work:

<input  type="hidden" id="00N0Y00000Agv5X" maxlength="100" name="00N0Y00000Agv5X" value="?=$visitorID?" size="100" type="text" />

You need to make sure that the “id” and “name” refer to the Custom Field on your Lead Object. You can generate the form quite easily using the “Web-to-Lead Setup” in Salesforce.  As we used Javascript to create the unique visitor ID within the Web Site, we will need to put that value into the input field using the following in your Web Site HTML:

     document.getElementById('00N0Y00000Agv5X').value=visitorID;

The basic set-up of communicating with Google Analytics is via a number of calls. Basically, Google will use a JSON Web Token for authentication. The basic sequence of messages is as follows:

Picture15

Create and sign JWT

To access the Google Analytics Reporting API, we need to use the Google Identity Platform to authenticate our request. The Google Identity Platform uses OAuth2 as an authentication mechanism, therefore sending a valid Private Key and providing a valid scope we should be given an access token that we can use for all subsequent requests to the desired scope.

Google Identity Platform Requires a JSON Web Token (JWT), made up of a Header, Claim Set and Signature (which contains the Header and Claim Set). We can communicate with the Google Identity Platform using the HTTP, HTTPRequest and HTTPResponse classes. This mechanism basically follows:

  1. Create the Request (We can use the HTTPRequest class for this purpose)
  2. Send the Request (We can use the HTTP class for this purpose)
  3. Get the Response (We can use the HTTPResponse class for this purpose)

First we need to create our classes and then set a few basic attributes.

// Some variables we will need.
 String key = '-=| YOUR KEY IS PASTED IN HERE |=-'; // Makes sure you put your private key in this string.
 String access_token;
 String token_type;
 String expires_in;
         
 // Create the HTTP, HTTPRequest and HTTPResponse objects.
 Http h = new Http();
 HttpRequest req = new HttpRequest();
 HttpResponse res = new HttpResponse();
 
 // Set the request end point.
 req.setEndpoint('https://www.googleapis.com/oauth2/v4/token');
 
 // Set the method that we'll use.
 req.setMethod('POST');
 
 // Set the content type that we'll be sending.
 req.setHeader('Content-Type','application/x-www-form-urlencoded');

Now we can construct our Header, Claim Set and Signature.

Header

The header consists of two fields that indicate the signing algorithm and the format of the assertion. Both fields are mandatory, and each field has only one value. As additional algorithms and formats are introduced, this header will change accordingly.

Service accounts rely on the RSA SHA-256 algorithm and the JWT token format. As a result, the JSON representation of the header is as follows {“alg”:”RS256″,”typ”:”JWT”}. The code to create the Header will look like this:

String header = '{"alg":"RS256","typ":"JWT"}';
 String header_encoded = EncodingUtil.base64Encode(blob.valueof(header));

Claim Set

The JWT claim set contains information about the JWT, including the permissions being requested (scopes), the target of the token, the issuer, the time the token was issued, and the lifetime of the token. Most of the fields are mandatory. Like the JWT header, the JWT claim set is a JSON object and is used in the calculation of the signature. Like the JWT header, the JWT claim set should be serialized to UTF-8 and Base64url-safe encoded.

String claim_set = '{"iss":"salesforce@core-crossing-158117.iam.gserviceaccount.com"'; // Email address of the Service Account
        claim_set += ',"scope":"https://www.googleapis.com/auth/analytics.readonly"'; // A space-delimited list of the permissions that the application requests.
        claim_set += ',"aud":"https://www.googleapis.com/oauth2/v4/token"'; // A descriptor of the intended target of the assertion. When making an access token request this value is always https://www.googleapis.com/oauth2/v4/token.
        claim_set += ',"exp":"' + datetime.now().addHours(1).getTime()/1000; // The expiration time of the assertion, specified as seconds since 00:00:00 UTC, January 1, 1970. This value has a maximum of 1 hour after the issued time.
        claim_set += '","iat":"' + datetime.now().getTime()/1000 + '"}'; //The time the assertion was issued, specified as seconds since 00:00:00 UTC, January 1, 1970.
 
 String claim_set_encoded = EncodingUtil.base64Encode(blob.valueof(claim_set)); // Enclose the Claim Set because we're going to transmit it across the Internet.

Breaking down the construction of the Claim Set, we have the following:

  • “iss”: the Service Account that will be used for authentication. This must exist in the Google API Management Service Accounts. You can get this from your Google API Console as suggested above, or it is also within the JSON Key file that holds the Private Key that was generated.
  • “scope”: The scope of services that the service account is requesting access to. For our purposes this will be https://www.googleapis.com/auth/analytics.readonly because we only need readonly access to analytics.
  • “aud”: A descriptor of the intended target of the assertion. When making an access token request this value is always https://www.googleapis.com/oauth2/v4/token
  • “exp”: The expiration time of the assertion, specified as seconds since 00:00:00 UTC, January 1, 1970. This value has a maximum of 1 hour after the issued time.
  • “iat”: The time the assertion was issued, specified as seconds since 00:00:00 UTC, January 1, 1970.

Signature

JSON Web Signature (JWS) is the specification that guides the mechanics of generating the signature for the JWT. The input for the signature is the byte array of the following content:

  • Base64url encoded Header
  • Base64url encoded Claim Set

The signing algorithm in the JWT Header must be used when computing the signature. The only signing algorithm supported by the Google OAuth 2.0

Authorization Server is RSA using SHA-256 hashing algorithm. This is expressed as RS256 in the alg field in the JWT Header.

String signature_encoded = header_encoded + '.' + claim_set_encoded; // Add the Header and Claim Set to the Signature
signature_encoded = signature_encoded.replaceAll('=',''); // Remove any "="
String signature_encoded_url = EncodingUtil.urlEncode(signature_encoded,'UTF-8'); // URL encode the signature for safe transmission.
blob signature_blob =   blob.valueof(signature_encoded_url);
blob private_key = EncodingUtil.base64Decode(key); // Decode the private key
String signature_blob_string = EncodingUtil.base64Encode(Crypto.sign('RSA-SHA256', signature_blob, private_key)); // Sign the UTF-8 representation of the input using SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function) with the private key obtained from the Google API Console. The output will be a byte array.
String JWT = signature_encoded + '.' + signature_blob_string; //The signature must then be Base64url encoded. The header, claim set, and signature are concatenated together with a period (.) character. The result is the JWT. 
JWT = JWT.replaceAll('=','');

Use JWT to Request Token

After generating the signed JWT, we can use it to request an access token. This access token request is a HTTPS POST request, and the body is URL encoded.

String grant_string= 'urn:ietf:params:oauth:grant-type:jwt-bearer';
req.setBody('grant_type=' + EncodingUtil.urlEncode(grant_string, 'UTF-8') + '&assertion=' + EncodingUtil.urlEncode(JWT, 'UTF-8'));
res = h.send(req);

Token Response

We can now look at the response from Google to determine the correct course of action.

String response_debug = res.getBody() +' '+ res.getStatusCode();
 if(res.getStatusCode() == 200) // informtaion on Error codes here: https://developers.google.com/drive/v3/web/handle-errors
 {
     System.debug('Google API|Success');
     
     // We have JSON returned to us, so we need to parse it. We're looking for access_token primarily.
     JSONParser parser = JSON.createParser(res.getBody());
     while (parser.nextToken() != null) 
     {
         if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'access_token'))
         {
              // Move to the value.
              parser.nextToken();
              // Get the access_token
              access_token = parser.getText();
              System.debug('Google API|Access Token|' + Access_Token );
         } // just debug stuff below, not needed for this to work, but interesting to see information in Debug.
         if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'token_type'))
         {
              // Move to the value.
              parser.nextToken();
              // Get the token type.
              token_type = parser.getText();
              System.debug('Google API|Token Type|' + token_type );
         }
         if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'expires_in'))
         {
              // Move to the value.
              parser.nextToken();
                     
              // Get the expires_in.
              expires_in = parser.getText();
              System.debug('Google API|Expires In|' + expires_in );
         }
     }
 }
 else
 {
     System.debug('Google API|Error|'+res.getStatusCode());
 }
         
 /*
  * We have the access token or not, so let's return what we have.
  */
 return access_token;

We can now use the Token to call Google API and ultimately reach Google Analytics data.

Use Token to Call Google API

Every call to the Google API will need to include the Token that we received using the above Apex code. Here is an example of a call to the API with a JSON report request for Google Analytics.

Here’s an example JSON:

{
 "reportRequests":
 [{
     "viewId": "-=|** PUT YOUR VIEW ID HERE **|=-",
     "dateRanges": 
     [{
         "startDate": "2017-01-01",
         "endDate": "2018-01-01"
     }],
     "metrics": 
     [{
         "expression": "ga:totalEvents"
     }], 
     "dimensions": 
     [
         {"name": "ga:eventAction"},
         {"name": "ga:eventLabel"}
     ],
     "dimensionFilterClauses":
     [
         {"filters":
         [{
             "dimensionName":"ga:dimension1",
             "operator":"EXACT",
             "expressions":["%VISITOR_ID%"]
         }]
         }
     ]
 }]
 }

The viewId is important, but not mentioned very much within the examples provided in Google Analytics. You can obtain the viewId from the Google Analytics Console. Select the “ADMIN” menu option and you should see three columns, “ACCOUNT”, “PROPERTY” and “VIEW”, select the option “View Settings” in the “VIEW” column, you should see something similar to:

Picture16

Use the value displayed underneath the “View ID” heading. Here is an example of Apex code to use an Access Token to pass JSON to request a report from Google Analytics.

/* To test/create JSON:
  * use https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet#try-it
  */   
 
 String json = '-=| PUT YOUR JSON REPORT REQUEST HERE |=-'; // Make sure you have injected the Visitor ID if you need to filer on a particular Lead.
         
 // Update the JSON to add the visitor ID that we'll need to filter the report in Google Analytics (visitorID is passed as parameter to Apex method).
 json = json.replace('-=|OBVIOUS STRING TO CHANGE|=-', visitorID);
         
 // Create the HTTP, request and response objects that we'll use.
 Http h = new Http();
 HttpRequest req = new HttpRequest();
 HttpResponse res = new HttpResponse();
         
 //Set-up the request details.
 req.setEndpoint('https://analyticsreporting.googleapis.com/v4/reports:batchGet?fields=reports/data');
 req.setMethod('POST');
 req.setHeader('Content-Type','application/json');
 req.setHeader('Content-Length', ''+json.length());
 req.setHeader('Accept-Encoding','gzip, deflate');
 req.setHeader('Authorization', 'Bearer ' + '-=| THE ACCESS TOKEN RECEIVED GOES HERE|=-');
 
 req.setBody(json);
 system.debug('Google Analytics|JSON ='+json);
 system.debug('Google Analytics|Body ='+req.getBody());
 
 //Send the request via the HTTP object capturing the response.
 res = h.send(req);       
         
 // Process the response if successful (https://developers.google.com/analytics/devguides/reporting/core/v4/errors)
 // This could be an exception handled from here, but for the sake of the PoC, we'll keep it simple.
 if(res.getStatusCode() == 200)
 {
     System.debug('Google Analytics|Success|'+res.getStatusCode());
     System.debug('Google Analytics|Response Body='+res.getBody());
             
     // Something could be done here to process the response.
 }
 else
 {
     // it's not a 200...so Google API was not happy; but we could handle some errors better, such as
     // 401 errors (unauthenticated) by requested a new access token and trying again because our access token may have expired.
     // or 429 errors (resource exhausted) by implementing exponential backoff so we stop overloading Google API (LIMITS!!!)
     System.debug('Google Analytics|Request Body='+req.getBody());
     System.debug('Google Analytics|Response Body=' + res.getBody());
     System.debug('Google Analytics|Error|'+res.getStatusCode());
             
     // Handle the errors if appropriate.
 }

That’s it. Hopefully everything should be working OK.

Leave a comment