Tracking Form Completion in Google Analytics With Redux

In this tutorial we’re going to collect analytics on a Redux-powered user
form. You will learn:

How to measure user drop off in forms using Google Analytics.
How to create a destination funnel report in Google Analytics.
How to map Redux actions to Google Analytics events and page views.

This tutorial assumes prior exposure to Git, JavaScript (ES2015), and Redux.
The App
We’ll be collecting analytics on the payment form of a simple eCommerce app. To
download the app, open a terminal and run the following command:
git clone git@github.com:rangle/analytics-sample-apps.git

Then navigate into the cloned directory and checkout v1.0.0:
cd analytics-sample-apps
git checkout tags/v1.0.0

Now, navigate to the shopping-cart directory and install the project’s
dependencies:
cd shopping-cart
npm install

Once npm has finished installing the app’s dependencies, start the app with the
following command:
npm start

The app should open in your browser at the following address:
http://localhost:3000/. Take a minute or two to play around and explore the
app:
1. / shows a list of items to buy
2. /cart shows items added to the cart
3. /payment shows a form for collecting payment details
4. /order-complete shows a message indicating a successful order
Our goal is to collect analytics on the payment form. Navigate to the /payment
view and open your browser’s JavaScript console.
Refresh the page, type a character into each input field, then click the
disabled Buy Now button. Your form and console should look something like
this:

When you land on the page Redux fires a ROUTE_CHANGED action, then an action
for each form field change, and finally an action when a user attempts to
buy something but fails to proceed because of invalid inputs.
Update the form with valid inputs this time. None of the form fields should have
a red outline and the Buy Now button should be enabled. Click the Buy Now
button. Notice how a Redux action fired whenever an input field changed, and
notice how there is one last ROUTE_CHANGED action when you successfully completed the form.

Here’s all you need to remember at this point:
* Our goal is to collect analytics on the payment form
* Redux actions fire when the route changes and when the user types something into the form fields

Setting Up Google Analytics
Now that we’ve had a look at the form let’s see how we can set up a report in
Google Analytics to show the percentage of users that saw the form, filled it
in, and successfully completed an order.
Sign up for Google Analytics if you haven’t already, and
create a new web property.
Make a note of your property’s tracking Id.
Follow the instructions
here
to create a new goal.
1. In Goal Setup, select Custom.
2. In Goal Description, enter in Payment Form Filled for the goal name.
3. In Goal Description, select Destination for the goal type.
4. Lastly, fill in Goal Details to match the following image then click Save.

Let’s review. Our goal is to reach a destination, which is the
/order-complete page. We set up a funnel report that shows the six steps we
expect the user to take before reaching the /order-complete page. We first
expect the user to land on the /payment page. Then we expect the user to fill
in each input field. We also expect some users might attempt to submit the
payment form with invalid inputs.
Notice anything strange? Funnel reports in Google Analytics expect each step a
user takes towards a goal is a whole new page. This is a remanent from the old
days when single page apps weren’t really a thing. Back then, whenever anything
major changed in a website, there was a page load. Now, in modern apps like the
one we’re working on, we’re using JavaScript to dynamically display different
views, update the address bar, and manage the browser history. Our user might
travel across different pages and fill in forms, but from the perspective of Google Analytics
nothing is happening. We need a way to fake a page load when these
things happen. Or more specifically, we need a way to map our app’s Redux
actions to Google Analytics page views.
Based on the funnel report we set up, here’s a map of Redux actions to the page
loads we need to fake:
Redux Action Type Virtual Page Load ROUTE_CHANGED /payment NAME_ENTERED /name-entered EMAIL_ENTERED /email-entered PHONE_NUMBER_ENTERED /phone-number-entered CREDIT_CARD_NUMBER_ENTERED /cc-number-entered BUY_NOW_ATTEMPTED /buy-now-attempted ROUTE_CHANGED /order-complete
Thankfully, there’s an npm package to help us with this exact problem.
Redux Beacon

Redux Beacon is a framework
agnostic library for mapping Redux actions to analytics events. In this next
part, we’ll look at how we can leverage its API to solve our form analytics
problem.
Let’s start off by installing the library and saving it to our project’s
package.json file. Open a terminal, cd into the shopping-cart directory
and run the following command:
npm install redux-beacon@0.2.x –save

Once that’s done, follow the instructions
here to
add your Google Analytics tracking snippet to the
shopping-cart/public/index.html file. This should be the same Google
Analytics property we set up in the previous section. If the app is
still running then saving the file should trigger a site
rebuild. Otherwise, call npm start to get the site up and running
again.
At this point, you should see one active user in your Google
Analytics Real-Time
dashboard.
Next, create a new file called analytics.js in shopping-cart/src and add the
following code to it:
// shopping-cart/src/analytics.js

import { createMiddleware } from ‘redux-beacon’;
import { GoogleAnalytics } from ‘redux-beacon/targets/google-analytics’;
import { logger } from ‘redux-beacon/extensions/logger’;

const pageview = {
eventFields: (action, prevState) => ({
hitType: ‘pageview’,
page: prevState.route.length > 0 ? action.payload : undefined,
}),
eventSchema: {
page: value => value !== undefined,
},
};

const eventsMap = {
ROUTE_CHANGED: pageview
};

export const middleware = createMiddleware(eventsMap, GoogleAnalytics, { logger });

Let’s go through this block by block.
import { createMiddleware } from ‘redux-beacon’;
import { GoogleAnalytics } from ‘redux-beacon/targets/google-analytics’;
import { logger } from ‘redux-beacon/extensions/logger’;

Here, we’re importing various functions provided by Redux Beacon. The second and
third imports are relative imports. Redux Beacon was built this way to help
minimize bundle sizes.
const pageview = {
eventFields: (action, prevState) => ({
hitType: ‘pageview’,
page: prevState.route.length > 0 ? action.payload : undefined,
}),
eventSchema: {
page: value => value !== undefined,
},
};

This block is called an event definition. It’s a plain old JavaScript object
(POJO) with a special eventFields method. The object returned by eventFields
is the analytics event that Redux Beacon will push to a target. There’s another
special property called eventSchema that contains a contract for the
properties returned by eventFields. Here our eventSchema expects the event
object to have a page key whose value is not undefined. If the eventFields
method returns an event whose page is undefined then Redux Beacon won’t push
anything to Google Analytics. Take a closer look at the eventFields
method. The page will only be undefined if the previous route’s length is
zero. And if you look at src/reducer.js the initial state for the route is an
empty string whose length is zero. So with this event definition we are telling
Redux Beacon to send a page hit to Google Analytics whenever the route changes
except on the initial page load.
Why wouldn’t we want to send a page hit to Google Analytics on the initial page
load? Because we’re already doing that in the tracking snippet. The last line
ga(‘send’, ‘pageview’) hits Google Analytics with a page view that matches the
initial route. If our event definition didn’t include the eventSchema then we
would be sending two page hits to Google Analytics instead of one when the app
first loads.
There’s one other course we can take to prevent the initial page load from being
recorded twice. You can just delete or comment out the ga(‘send’, ‘pageview’)
line from the tracking snippet. In which case the event definition would no
longer need an eventSchema and the eventFields method would lose some fat:
const pageview = {
eventFields: action => ({
hitType: ‘pageview’,
page: action.payload,
})
};

const eventsMap = {
ROUTE_CHANGED: pageview
};

This block is called an event definitions map. This is where we link Redux
actions to event definitions. In this case when the ROUTE_CHANGED action
fires, Redux Beacon will call the pageview’s eventFields method, pass it the
action object, then push the resulting event object to Google Analytics. Let’s
have a look at an example:

The app dispatches the { type: ‘ROUTE_CHANGED’, payload: ‘/cart’ } action
Redux Beacon calls pageview.eventFields with the action
The eventFields method returns { hitType: ‘pageview’ page: ‘/cart’ }
Redux Beacon hits Google Analytics with a page view (/cart)

export const middleware = createMiddleware(eventsMap, GoogleAnalytics, { logger });

This last line creates the Redux Beacon middleware that we’re going to apply to
the app’s Redux store. We’re first passing in the event definitions map,
followed by the target for the resulting events, and finishing off with a
logging extension so we can see the analytics events that Redux Beacon
generates.
Now that we have Redux Beacon set up all we have to do is apply the middleware
when creating the Redux store. Add the following line to the src/App.js file.
// src/App.js (somewhere near the top of the file)
import { middleware as analyticsMiddleware } from ‘./analytics’;

Then, scroll down to where createStore is called and apply the middleware.
// src/App.js (should be around line 35)
const store = createStore(
reducer,
applyMiddleware(createLogger(), analyticsMiddleware)
);

Save the file, then mosey over to your browser and refresh the shopping cart
app. Navigate from the root page to the cart page then to the payment page. Have
a look at the console. Like before, you should see Redux actions for each route
change, but now you should also see the associated analytics events above each
Redux action.
Go back to the Real-Time view in Google Analytics, this time select the
Behaviour tab and click on Page views (last 30min). You should see /,
/cart, and /payment listed along with the number of times each route was
visited.

With one simple event definition we managed to map every ROUTE_CHANGED action
to a Google Analytics page hit. This includes the two page hits we need for our
funnel report.
Redux Action Type Virtual Page Load ROUTE_CHANGED /payment NAME_ENTERED /name-entered EMAIL_ENTERED /email-entered PHONE_NUMBER_ENTERED /phone-number-entered CREDIT_CARD_NUMBER_ENTERED /cc-number-entered BUY_NOW_ATTEMPTED /buy-now-attempted ROUTE_CHANGED /order-complete
To track the remaining page views needed for the funnel report we need
to create an event definition for the each form field Redux action,
and lastly an event definition for when users try to submit the form with
invalid inputs.
In src/analytics.js add the following function below the pageview
event definition.
function createPageview(route) {
return {
eventFields: () => ({
hitType: ‘pageview’,
page: route,
title: ‘Payment Form Field’,
}),
};
}

This factory function returns an event definition for a Google Analytics page
hit. We have added a title property to the event to help distinguish these
page views from the rest. We’ll see the usefulness of this later on.
Next, update the eventsMap to include the form field Redux actions.
const eventsMap = {
ROUTE_CHANGED: pageview,
NAME_ENTERED: createPageview(‘/name-entered’),
EMAIL_ENTERED: createPageview(‘/email-entered’),
PHONE_NUMBER_ENTERED: createPageview(‘/phone-number-entered’),
CREDIT_CARD_NUMBER_ENTERED: createPageview(‘/cc-number-entered’),
};

Here we’re using the createPageview factory to create an event
definition for each input field event.
Go back to your browser, navigate to the app’s /payment page and fill in the
form. You should see analytics events being logged to the console whenever the
form fields change. This is what we wanted right? Yes and no. We wanted to hit
Google Analytics with a page view when a user enters their name, email, phone
number, or credit card number. But with our current setup, we are hitting
Google Analytics with page views each time the user adds a single character to
each form field. So if the user enters John in the name field, Google
Analytics records four hits to /name-entered instead of one.
The eventSchema can help us with this problem. Update the createPageview
factory in src/analytics.js as follows.
function createPageview(fieldName, route) {
return {
eventFields: (action, prevState) => ({
hitType: ‘pageview’,
page: prevState[fieldName].length === 0 ? route : ”,
title: ‘Payment Form Field’,
}),
eventSchema: {
page: value => value === route,
},
};
}

We updated the event definition returned by createPageView with a new
property: eventSchema. Here our eventSchema expects the event object to
have a page key whose value is equal to the route. If the value is not equal
to the route then Redux Beacon won’t push anything to Google Analytics. Now look
at the changes made to the eventFields method, you will notice a new
conditional that ensures the page property will only match the route when the
form field’s value is first being filled. That is, if the previous state of the
form field is empty (it’s length equals zero) then the page property is set to
the route, otherwise the page property is set to an empty string.
Update the eventsMap to use the revised event definition factory.
const eventsMap = {
ROUTE_CHANGED: pageview,
NAME_ENTERED: createPageview(‘name’, ‘/name-entered’),
EMAIL_ENTERED: createPageview(’email’, ‘/email-entered’),
PHONE_NUMBER_ENTERED: createPageview(‘phoneNumber’, ‘/phone-number-entered’),
CREDIT_CARD_NUMBER_ENTERED: createPageview(‘ccNumber’, ‘/cc-number-entered’),
};

Save the file, head on over to your browser, refresh the app, and navigate to the payment form. Did it work? Before, Redux Beacon would hit Google Analytics with a pageview each time a form field’s value changed. Now, Redux Beacon only sends one page hit per form field.
Redux Action Type Virtual Page Load ROUTE_CHANGED /payment NAME_ENTERED /name-entered EMAIL_ENTERED /email-entered PHONE_NUMBER_ENTERED /phone-number-entered CREDIT_CARD_NUMBER_ENTERED /cc-number-entered BUY_NOW_ATTEMPTED /buy-now-attempted ROUTE_CHANGED /order-complete
Almost done! There’s one more event we need to capture before we can call it a day. We are tracking each route change, we are tracking the number of times a user fills in their name, email, phone number, and credit card number. Now we need to track the number of times a user tries to submit the payment form with
invalid inputs.
Update the eventsMap with an event definition for the BUY_NOW_ATTEMPTED
action.
const eventsMap = {
ROUTE_CHANGED: pageview,
NAME_ENTERED: createPageview(‘name’, ‘/name-entered’),
EMAIL_ENTERED: createPageview(’email’, ‘/email-entered’),
PHONE_NUMBER_ENTERED: createPageview(‘phoneNumber’, ‘/phone-number-entered’),
CREDIT_CARD_NUMBER_ENTERED: createPageview(‘ccNumber’, ‘/cc-number-entered’),
BUY_NOW_ATTEMPTED: {
eventFields: () => ({ hitType: ‘pageview’, page: ‘/buy-now-attempt’ })
}
};

Redux Action Type Virtual Page Load ROUTE_CHANGED /payment NAME_ENTERED /name-entered EMAIL_ENTERED /email-entered PHONE_NUMBER_ENTERED /phone-number-entered CREDIT_CARD_NUMBER_ENTERED /cc-number-entered BUY_NOW_ATTEMPTED /buy-now-attempted ROUTE_CHANGED /order-complete
That’s it! We mapped all the Redux actions required for the form’s funnel report! Now, Google Analytics should receive all the data required to fill the Conversions> Goals > Funnel Visualization report. It’s worth mentioning that the funnel visualization report is not real-time, so it might take a few hours before you start seeing any data. Until then, here’s a teaser as to what it might eventually look like:

A Word of Caution
There’s one thing to be aware of when tracking form completion in Google
Analytics. The virtual page views we created for each form field have become a
part of the total page view counts and user flows. This isn’t usually what we
want. One way around this problem is to create a new view for your Google
Analytics property with the form field virtual page views filtered out. To do
so, follow the instructions
here to create a new
view for your property. Once you have a new view, follow the instructions
here
to create a new custom filter. When creating the custom filter set the filter
type to Exclude, set the filter field to Page Title, set the filter pattern
to Payment Form Field, then click Save. The reports in this view will
contain every page view hit except those related to the payment form.
Before You Go
Here’s what we’ve achieved:
– We learned how to create a destination funnel report in Google Analytics.
– We learned that Google Analytics expects most site changes to be triggered by page loads.
– We learned how to use Redux Beacon to map Redux actions to analytics events.
– We learned how to validate analytics events before sending them to Google Analytics.
– We learned that there are some caveats to using Google Analytics for tracking form completion.

Link: http://blog.rangle.io/tracking-form-completion-in-google-analytics-with-redux/