Tracking User Interactions And Analytics With Small Abstractions In AngularJS
In order to get a sense of how people are using our JavaScript applications at InVision, it's important to track various user interactions. Doing so can expose popular workflows, usability issues, and blind-spots; and, in general, allow us to make future decisions based on actual data. When I first started adding tracking code to our AngularJS applications, it was ugly and noisy and made the code harder to read. But, over time, I started to factor out my tracking code into small abstractions - little Functions that would hide the complexity of the tracking logic. This gave me tiny, local APIs that I could invoke from within both my Controllers and my Views. I've enjoyed this pattern quite a bit; so, I thought I would put together a small demo in AngularJS 1.2.22 (though, the version is really irrelevant).
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
When it comes to analytics, I'm basically a caveman. I don't really understand anything about "funnels" and critical workflows and conversion pipelines. I just know how to build a View and track click events. I leave the complex stuff to the Data Scientists who know how to use all the fancy analytical software.
So, when it comes to the actual tracking, I try to track as little as possible in order to keep the code as simple as possible. I'm not one for adding arbitrary attributes just in case we may need to report on them one day. I'd rather know why something is being tracked; and, if we can't say why, then I don't track it.
That said, even with minimal tracking, the call to track an event is still relatively "wordy". Here's what an average call in our application looks like:
analytics.track(
"ben@bennadel.com",
"DemoView.Action",
{
Action: "View loaded",
Role: "Admin",
IsEnterprise: false
}
);
Even at just 9-lines of code, it's easy to see why adding a call like this in a bunch of Functions would make the code super noisy and much harder to read. To keep complexity under control, I've gotten into the habit of taking this call and moving it into a component-local Function that takes a single "Action" argument:
function trackUserInteraction( action ) {
analytics.track(
"ben@bennadel.com",
"DemoView.Action", // This is the component-specific event.
{
Action: action, // This is the dynamic bit.
Role: "Admin",
IsEnterprise: false
}
);
}
This approach automatically tracks each interaction under a component-specific / view-specific event (DemoView.Action
in this case); and then tracks the low-level interaction as a property of the event. Obviously, this implementation is specific to the analytics library that we use; but, in general, what I'm trying to do here is factor-out all the stuff that can remain reasonably static within a single View of the application.
I then take this Function / abstraction and expose it as a Public method in my component such that I can consume it both internally to the code-behind as well as externally within my view-template. To this in action, I've put together a simple demo in which I render a list of Friends and then track interactions with the view.
Note that I'm calling my trackUserInteraction()
function in the HTML template using Angular's ng-click
directive; and, I'm also calling it directly from within some click-handlers.
<!doctype html>
<html lang="en" ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Tracking User Interactions And Analytics With Small Abstractions In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css">
</head>
<body ng-controller="appController">
<h1>
Tracking User Interactions And Analytics With Small Abstractions In AngularJS
</h1>
<ul class="items">
<li ng-repeat="friend in friends track by friend.id" class="items__item">
<a
ng-href="#/friend/{{ friend.id }}"
ng-click="trackUserInteraction( 'View friend' )">
{{ friend.name }}
</a>
—
<button ng-click="editFriend( friend )">
Edit
</button>
<button ng-click="deleteFriend( friend )">
Delete
</button>
</li>
</ul>
<!-- ---------------------------------------------------------------------------- -->
<!-- ---------------------------------------------------------------------------- -->
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/3.6.0/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
</script>
<script type="text/javascript">
app.controller( "appController", AppController );
function AppController( $scope, analytics ) {
$scope.friends = [];
init();
// Expose the public methods.
$scope.deleteFriend = deleteFriend;
$scope.editFriend = editFriend;
$scope.trackUserInteraction = trackUserInteraction;
// ---
// PUBLIC METHODS.
// ---
// I take the user to the delete confirmation form for the given friend.
function deleteFriend( friend ) {
trackUserInteraction( "Delete friend" );
// TODO: Delete friend (outside the scope of demo).
// ....
// ....
}
// I take the user to the edit form for the given friend.
function editFriend( friend ) {
trackUserInteraction( "Edit friend" );
// TODO: Edit friend (outside the scope of demo).
// ....
// ....
}
// I track the given user interaction / action.
function trackUserInteraction( interaction ) {
// Instead of spreading this complexity throughout the component, I like
// to create one (or two) methods in the component that hide this
// complexity. Then, I just call these methods from within the component
// controller and from within component view.
analytics.track(
"ben@bennadel.com",
"DemoView.Action",
{
Action: interaction,
Role: "Admin"
}
);
}
// ---
// PRIVATE METHODS.
// ---
// I get called once before the component is mounted.
function init() {
trackUserInteraction( "Open" );
$scope.friends = [
{ id: 1, name: "Kim" },
{ id: 2, name: "Joe" },
{ id: 3, name: "Sam" },
{ id: 4, name: "Ryan" },
{ id: 5, name: "Kit" }
];
}
}
</script>
<script type="text/javascript">
app.service( "analytics", AnalyticsFactory );
function AnalyticsFactory() {
// Expose the public methods.
return({
track: track
});
// ---
// PUBLIC METHODS.
// ---
// I record the given analytics event.
function track( userID, eventType, eventProperties ) {
console.group( "%cAnalytics Events", "font-weight: bolder ; color: dodgerblue ; text-decoration: underline ;" );
console.log( "User:", userID );
console.log( "Event:", eventType );
console.log( "Properties:", eventProperties );
console.groupEnd();
}
}
</script>
</body>
</html>
As you can see, by moving all the analytics tracking into a component-local Function, I keep the "noise" down to a minimum. And because I'm defining this abstraction at the component-level - not at the library level - I still get all the flexibility that I need across components within the greater application.
If we run this JavaScript application in the browser and click around, we can see the analytics events being logged in the console:
As you can see, each interaction is being logged as a robust event even though the only attribute that I'm providing in the various calls is the one, little Action
property. Nice and clean!
Adding analytics and tracking is messy. There's no way to get around it. It dilutes the meaning of any code to which it is added. Over time, I've found that I can keep the readability of the code relatively high by factoring-out all the tracking code that doesn't change, leaving me with tiny Function abstractions. The abstractions also help curb the amount of junk that I try to track in the first place. Which is a Good Thing.
Want to use code from this post? Check out the license.
Reader Comments
Why you have removed my coments?
You like a little girl, you can't read critic and that's why you are not professional.
BTW AngularJS is DEAD, and write about it in 2021 is stupid
@Alex,
If you want me to leave your comments, then make them constructive. Instead of just insulting me abstractly, make a suggestion on how the code can be improved. You seem to have an issue with the AngularJS portion; though, there is very little about Angular in this post at all - the post focuses on the pattern of tracking interactions while trying to also minimize the amount of noise that is added to the code.
How would you improve this in a meaningful way?
First question, why do I need to improve this?
It's a spaghetti code with a lot of mistakes with old js library which is dead?
Second question, if you do not know how to write good code why do I need to teach you?
And third you need to learn new libraries and improve your code, I can't look in it without pain..
Aw Alex... has someone pissed on yer chip pal?
Gentle advice... perhaps invest yer time on a bit of introspection rather than slagging off someone who gives a lot back to his community and is a pretty nice bloke.
Maybe consider this: you're making yourself look like a bit of a drip in the way yer conducting yerself. Was that your intent? I doubt it. Nothing you've said is a reflection on Ben. It's just a reflection on you. Seriously: give that some thought.
Take care eh?
@Adam, everything I said related to Ben. And if you a worked on him, than it's a time to find a new job
@Adam becouse everything you wrote cost nothing as you