Server GTM Promise API. How to get / set user state and pass it to Google Ads Remarketing

Server GTM Promise API

The goal

In this post I’ll show how to use Promise API to get user state, modify a few user properties and pass these properties to Google Ads Remarketing Tag.

How to do it

  1. Send a login event from a Web GTM container to Server GTM;
  2. On Server GTM get user state by API. Get loginCount and sessionsCount values from a user state. These two names are only for learning purposes - to show Promise.all in all it’s beauty;
  3. Increase loginCount, sessionsCount by one;
  4. Save new user state by API;
  5. Generate a new event with changed loginCount, sessionsCount;
  6. Fire Google Ads Remarketing Tag on this event and pass loginCount, sessionsCount to Google.

User state API

Let’s pretend that IT department provided us an API to deal with user property:

  • To get all user data: GET https://…${userId}/data/
  • To set user property PUT https://…${userId}/data/${propertyName} with body {propertyName: propertyValue}
  • And API has simple authorization by token.

But if you don’t have special API, you can store user state in Google Firestore, please read how to use Firestore and GTM together.

Implementation

Step 1. It’s easy because it was done all the time before Server GTM.

Step 2 - 5. Let’s create a ServerSide GTM Template. Later we will add a Tag based on this template and will fire this tag on the login event. The template will implement steps from 2 - 5.

First I’ll show all the code and later we will go line by line:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const getAllEventData = require("getAllEventData");
const getEventData = require("getEventData");
const logToConsole = require("logToConsole");
const makeNumber = require("makeNumber");
const Object = require("Object");
const Promise = require("Promise");
const returnResponse = require("returnResponse");
const runContainer = require("runContainer");
const sendHttpRequest = require("sendHttpRequest");
const JSON = require("JSON");

// Helpers

// Get endpoints
const getGetEndpoint = () => {
  return "https://some_url" + userId + "/data/";
};

const getPutEndpoint = (propertyName) => {
  return "https://some_url" + userId + "/data/" + propertyName;
};
// get headers
const getHeaders = () => {
  return {
    Authorization: 'Token token="' + token + '"',
    "Content-type": "application/json",
  };
};

// Some cleaning logic for user state
const prepareBody = (body) => {
  const userData = JSON.parse(body);
  // do some logic here
  let userData = {};
  data.forEach((element) => {
    const keys = Object.keys(element);
    if (!userData.hasOwnProperty(keys[0]))
      userData[keys[0]] = element[keys[0]];
  });
  return userData;
};

// Prepare params for PUT request
function getPutRequestParam(propertyName, propertyValue) {
  let body = {};
  body[propertyName] = propertyValue;
  return {
    body: JSON.stringify(body),
    endpoint: getPutEndpoint(propertyName),
  };
}

// Main functions

// Increase properties from propertyNames
function increaseProperties(userData, propertyNames) {
  // Prepare request params for each property we want to change
  let requestsParams = [];
  propertyNames.forEach((propertyName) => {
    if (userData.hasOwnProperty(propertyName)) {
      // Increase params also in user state
      userData[propertyName] = makeNumber(userData[propertyName]) + 1;
    } else {
      userData[propertyName] = 1;
    }
    requestsParams.push(
      getPutRequestParam(propertyName, userData[propertyName])
    );
  });

  // Change all properties
  if (requestsParams.length > 0) {
    Promise.all(
      requestsParams.map((requestsParam) =>
        sendHttpRequest(
          requestsParam.endpoint,
          { headers: getHeaders(), method: "PUT", timeout: 5000 },
          requestsParam.body
        )
      )
    )
      .then((results) => {
        if (
          results.filter((result) => result.statusCode === 200).length ===
          requestsParams.length
        ) {
          // Add current user state to event
          Object.keys(userData).forEach((key) => {
            if (!allEventData.hasOwnProperty(key))
              allEventData[key] = userData[key];
          });
          //   Set new event name
          allEventData.event_name = "login_init";
          //   Run new event
          runContainer(allEventData, () => returnResponse());
          data.gtmOnSuccess();
        } else {
          logToConsole("Counters not set");
          data.gtmOnFailure();
        }
      })
      .catch((error) => {
        logToConsole("increaseProperties error", error);
        data.gtmOnFailure();
      });
  } else {
    logToConsole("requestsParams are empty");
    data.gtmOnFailure();
  }
}

// Get user data and pass to callback
function getData(callback) {
  sendHttpRequest(
    getGetEndpoint(),
    { headers: getHeaders(), method: "GET", timeout: 5000 },
    ""
  )
    .then((result) => {
      const userData = prepareBody(result.body);
      callback(userData);
    })
    .catch((error) => {
      logToConsole("getData error", error);
      data.gtmOnFailure();
    });
}

// Callback to change user properties
function init(userData) {
  increaseProperties(userData, ["loginCounter", "sessionCounter"]);
}

// Set token for API auth
const token = "SECRET_TOKEN";

//  Get event params
const userId = getEventData("user_id");
const allEventData = getAllEventData();

// Without user_id we can't get or modify user properties
if (!userId) {
  logToConsole("Event doesn't have userId");
  data.gtmOnFailure();
}
else{
  // Do the logic
  getData(init);
}

At the beginning of the code we require some API and declare all functions we will use. Let’s start from lines 137 - 149:

137
138
139
140
141
142
143
144
145
146
147
148
149
//  Get event params
const userId = getEventData("user_id");
const allEventData = getAllEventData();

// Without user_id we can't get or modify user properties
if (!userId) {
  logToConsole("Event doesn't have userId");
  data.gtmOnFailure();
}
else{
  // Do the logic
  getData(init);
}

Here we take userId from the event, If userId is undefined then we can do nothing. Also don’t forget to call data.gtmOnFailure(); for each error case.

If userId exists, call the getData and pass a callback function to it.

Next lines 112 - 127 getData function declaration:

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Get user data and pass to callback
function getData(callback) {
  sendHttpRequest(
    getGetEndpoint(),
    { headers: getHeaders(), method: "GET", timeout: 5000 },
    ""
  )
    .then((result) => {
      const userData = prepareBody(result.body);
      callback(userData);
    })
    .catch((error) => {
      logToConsole("getData error", error);
      data.gtmOnFailure();
    });
}

The function uses sendHttpRequest API then result is ready, clear userDate and pass it to callback. If you don’t understand what’s going on here the best starting point is the Simo’s post about ASYNCHRONOUS VARIABLES IN SERVER-SIDE GOOGLE TAG MANAGER. Yes, you are right Simo is doing great work for the whole GTM / GA / GCP and so on community that’s why I decided to uppercase his article (and also I just copied the title as it is).

Next in lines 129 - 132 we define simple callback function.

129
130
131
132
// Callback to change user properties
function init(userData) {
  increaseProperties(userData, ["loginCounter", "sessionCounter"]);
}

It calls increaseProperties methods and passes an array of properties we want to increase.

increaseProperties takes lines 55 - 110. Let’s break it into parts. The first part is

57
58
59
60
61
62
63
64
65
66
67
68
69
  // Prepare request params for each property we want to change
  let requestsParams = [];
  propertyNames.forEach((propertyName) => {
    if (userData.hasOwnProperty(propertyName)) {
      // Increase params also in user state
      userData[propertyName] = makeNumber(userData[propertyName]) + 1;
    } else {
      userData[propertyName] = 1;
    }
    requestsParams.push(
      getPutRequestParam(propertyName, userData[propertyName])
    );
  });

We increase all properties by one if it already exists but set it to one if it’s a new property. Also we prepare body and endpoint for each PUT request and add them to requestsParams array.

Next we use Promise.all:

71
72
73
74
75
76
77
78
79
80
81
  // Change all properties
  if (requestsParams.length > 0) {
    Promise.all(
      requestsParams.map((requestsParam) =>
        sendHttpRequest(
          requestsParam.endpoint,
          { headers: getHeaders(), method: "PUT", timeout: 5000 },
          requestsParam.body
        )
      )
    )

Here’s the beauty of the Promise API. No more inherent callbacks or other weird nasty things. Just clear Promise interface. I promise you will like it.

And then use then() lines 82 - 101:

 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
      .then((results) => {
        if (
          results.filter((result) => result.statusCode === 200).length ===
          requestsParams.length
        ) {
          // Add current user state to event
          Object.keys(userData).forEach((key) => {
            if (!allEventData.hasOwnProperty(key))
              allEventData[key] = userData[key];
          });
          //   Set new event name
          allEventData.event_name = "login_init";
          //   Run new event
          runContainer(allEventData, () => returnResponse());
          data.gtmOnSuccess();
        } else {
          logToConsole("Counters not set");
          data.gtmOnFailure();
        }
      })

First we check all requests return status 200 (sometimes it can be other 2XX statuses for example 201). Next add all user data to event data. Set new name and run runContainer API to generate an event with a new name.

That’s all! Now we’ve increased user properties and got the new event with all needed user data. The fun part is ending. Let’s go to the boring “profit part”.

Step 6. Let’s add two Event Data Variables for the two user properties we’ve just set.

Event Data Variable

Add trigger for the new event name

Trigger for the new event name

And add Google Ads Remarketing Tag

Google Ads Remarketing Tag with custom user properties

Profit. Now your marketing team is happier than ever as they can build audiences in Google Ads based on new properties. And your Dev team is happy too, as you move out from the front end all these “useless pixels” which only slow down our cool site.

P.S. If you, like me, are a great fan of Testing API you can read my next post about the way we can test Promise API. See you there!