Full Stack Marketing

Full Stack Marketing is the concept of building analytics into your tech stack.  Why would you do that? Fundamentally we want to answer the question “Do you know what your users are doing?“. And more importantly – figure out how to get users to do what you want them to do. If we focus on measuring AARRR metrics, we can write code to optimize them.

Our Analytics stack has four parts – Track, Measure, Test, and Report.

Track

Google Analytics (GA) tracking is pretty straightforward but can get complicated fast. You can do Analytics tracking on client and server side. Client-side tracking is the most prevalent and now uses gtag.js (formerly analytics.js). However, many ad blocking and/or privacy plugins will block this script from reporting. Therefore if you want bulletproof tracking you’ll need to use the server-side tracking implementing the Google Analytics Measurement Protocol.

Basic tracking includes simple page view tracking – allowing you to see some engagement information about your users. Beyond the out-of-the-box functionality tracking pageviews, we can utilize Event Tracking, campaign tracking, conversion tracking, and remarketing among other custom functionality.

Javascript Example

Here’s a very basic example of sending event tracking information to Google Analytics on a button click event.

This seems innocent enough and can scale decently using CSS selectors to bind events to. However this can get unwieldy fast.

At some point you might end up where I did – with a server side tracking include that contained all kinds of conditional rules, tracking scripts, and very little documentation, versioning, or context.

Enter Google Tag Manager.

Google Tag Manager

Google Tag Manager (GTM) is a tag management system to manage Javascript and HTML tags for tracking and analytics on websites. Now you can say goodbye to global-tracking.php or tracking.js where you dump a bunch of jQuery event handlers to manually track specific events in GA.

GTM has the concept of tags and triggers. Tags are the definition of what you are tracking and triggering is what action occurs that will send that definition to GA. Triggers can be clicks, scrolls depth, page view duration, element visibility, and many other engagement events.

GTM has many other interesting aspects to it beyond making it easy to define events and triggers for tracking. With GTM (and gtag.js now) you get access to the Data Layer which is essentially a Javascript array you can manipulate on the client side to do some clever tracking and data manipulation.

Pro Tip: Google Tag Manager is very powerful. It’s my belief this should be owned by someone technical as it allows anyone to inject ANY HTML/Javascript into your website. This power can be abused and/or inadvertently error prone.

What To Track

This is a common question and there are infinite answers but the best is “it depends”. It depends on your site, audience, goals, conversions, etc. Here are some quick ideas.

  • Video Views
  • Button Clicks
  • Dropdown Clicks
  • Navigation Clicks
  • Search Clicks
  • Image Views
  • Micro-conversions – add to cart, add to wishlist, fave, print, save
  • Scrolling

Testing

How do we know everything is working? You can use the Google Analytics Debugger Chrome extension to log all GA tracking events to your Dev Console. This is hugely valuable and a big time saver. You can also see client-side tracking in your GA realtime dashboard. Google Tag Assistant is another Chrome extension you can add to your full stack analytics tool belt. Tag Assistant will help you troubleshot tags and test various implementation scenarios.

Pro Tip: Setup a separate Google Analytics view for testing and debugging so you don’t skew your production analytics data.

Measure

You can track everything but measure nothing. This is the reason we are employing an Analytics stack. Tracking is just our fundamental base layer of data. Once we have data tracking we now need to make meaning of it.

If you are tracking micro-conversions, you can measure the number over time to see if you are improving. You can measure which source yields the most amount of micro-conversions using attribution in your funnel. You can measure performance across devices. You can measure if people are consuming your content or if they are using desired functionality. The list of possibilities is endless, however you should focus on a few otherwise you will succumb to analysis paralysis.

Google Analytics Goals

GA is the tool of choice in this Analytics stack for measuring – specifically Goals. You can use goals to measure how often users complete specific actions. Google Analytics Goal and Conversion tracking is the most under utilized feature in GA – but it’s super easy to setup. Most people just setup GA and let it run on the default settings, however 20-30 minutes of time can get you much further along.

Goals are essentially measurements of how users complete specific actions in your app or site. Common Goals include successful registration, finding and clicking on a search result, reading + scrolling through an entire page of content, or even just clicking on a button. You can measure the flow to goals through your apps in a visual funnel. You can attribute anything higher in your funnel to a goal – such as an acquisition channel? What does this mean in non-marketing speak? It means you can see if more people successfully registered on your site when they came from a PPC ad versus organic search. It means you can show that all the traffic to your site from Facebook is garbage. Measuring goals helps you create meaning to the endless swaths of data you are tracking.

What To Measure

  • Number of events / conversions / micro-conversions over time
  • Goals / Conversions
  • App / Feature Usage
  • API usage
  • User engagement

Test

By now we are tracking events. We have goals setup. And we are measuring the completion of those goals.

Now the fun is just beginning. We have all the tools in place to start experimenting. Testing in my world isn’t doing QA, it’s doing a/b tests. What variation of a page can yield the most goal completions? That leads us to the third tool in our Analytics stack – Google Optimize.

Google Optimize

Google Optimize (GO) is, among other things, a very well designed A/B testing suite for websites. GO is one of my favorite tools to implement rapid A/B testing. If you’ve ever written client or server side code to do feature toggling you will love using this. GO removes all the technical hurdles for testing variants of a website. It’s as simple as dropping in some JS (preferably via GTM) and setting up an experiment.

A key factor of a successful A/B test is having data and KPI’s to measure against. If you’ve made it this far, you are ready to roll.

What To Test

Lets go back to the micro-conversions example and for the sake of an easy concept, visualize a recipe website. We are probably tracking page views, clicks on images, clicks on ingredients, and clicks on saving the recipe for later. Our micro-conversion is the user saving the recipe for later so we start measuring that. Cool, now what? Test variations of your website to see if any design or UX changes increase that number.

You could test many things (multivariate test) or just two versions (split test). Again, to make it easy, imagine setting up an A/B split test to see if having a blue or red button yields more recipes saved by users.

GO will let you know when you reach a big enough data set to reveal a statistically relevant conclusion. Now instead of guessing what button color to use, you’ve used data to make that decision for you. This concept can be extrapolated as you formulate a full digital strategy.

Report

We are tracking, measuring, and testing. Now what? Chances are someone wants (or at least should) your findings.

Nobody likes creating reports. I’m not sure anybody even likes reading reports. But what’s the point of measuring and testing if you aren’t acting on the results? Reporting gives you the opportunity to create insights into how your app is being used as well as fuel more tests to prove new hypotheses. I’m going to show you two things. A reporting tool that doesn’t suck and a way to consume reports that doesn’t suck.

Google Analytics has a pretty decent set of reporting tools. Google Analytics Realtime shows what’s happening right now on your site. We’ve seen this earlier when testing GA events. We put this up on a big tv in our office on full screen rotating through various dashboards. I don’t have to remember to always go look at reports – I see it whenever I go get coffee. You can also setup automated daily, weekly, monthly reporting customized to things important for you to track.

But that just gets you started. The recommended tool for reporting in your Analytics stack is Google Data Studio (GDS).

Google Data Studio turns your data into informative dashboards and reports that are easy to read, easy to share, and fully customizable. Dashboarding allows you to tell great stories to support better business decisions.

GDS is a powerful Business Intelligence tool that you can use to create informative dashboards reporting on your digital tracking, measurement, and testing initiatives. With multiple datasources and some pre-built templates you can build out a nice dashboard suite in under an hour. You can hook up Search Console, Google Analytics, Google AdWords, and dozens of other sources. Furthermore you can build out blended reports with data from multiple sources.

What To Report

  • Test Results / Recommendations
  • Channel Attribution
  • Conversion / Funnel Tracking
  • User Engagement

Analytics Newsletter

Want to dive deeper on full stack marketing? Sign up for future Analytics insights here.



Presentation

This presentation is accompanied by a demo site at https://analyticsdemo.xyz. There you can see client side event tracking in action with vanilla JS and Vue.js using the Google Analytics Debugger extension. Download the site on Github to see the code behind the demo.

Resources

Dev Libraries / APIs

Server Side

Google Tag Manager

Google Optimize

Building a Referral System with Laravel

I recently launched a small product for Amazon Affiliates built with Laravel. At the heart of the application is a referral system. Users get bonus credits when they refer another user while getting recurring credit every time their referral uses the site. To tackle this the Laravel way, I got to use Requests, Middleware, and Cookies. I also integrated Google login using Socialite but that’s another post entirely.

Side Note For Context: Since Amazon Affiliates cannot use their own affiliate codes for purchases they make themselves, I built Associate Swap. When you search Swap for Amazon products it will grab a random Affiliate code from the database and add it to the URL. The more you use swap and refer other affiliates to swap the greater chance your code will be picked. The referral system is the heart of the growth and retention model.

Referral Use Case

When a user signs up, they are automatically generated a unique affiliate_id. This id is will be used to generate unique URLs they can share to be credited for another user signing up and using the site – ie https://associateswap.com/?ref=17FbCjzIzj.

Getting Started

I’m going to assume you’ve already scaffolded your Authentication using the make:auth artisan command.

$ php artisan make:auth

Using this command you will now have a User model and associated Migrations. In order to track who referred who we need to add a couple of fields to our User object and associated database table. Time to generate a Migration.

$ php artisan make:migration AddAffiliateTrackingToUsersTable

Open up the migration file that was just generated, and add the fields we need to track users in the referral system.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddAffiliateTrackingToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function(Blueprint $table)
        {
            $table->string('referred_by')->nullable();
            $table->string('affiliate_id')->unique();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function(Blueprint $table)
        {
            $table->dropColumn('referred_by');
            $table->dropColumn('affiliate_id');
        });
    }
}

Now is a good time to run the Migrations on your database… If you haven’t run this since make:auth then it will create the User Model and Table. And with your additional Migration it will add your new fields referred_by and affiliate_id.

$ php artisan migrate

If you stop reading now and skip ahead you are going to run into an annoying error. If you jump to the Cookie generation and the Middleware part of the post (I mean c’mon who can’t wait until the Middleware section) you are going to bypass a commonly missed Laravel step. Update your model to make your new fields fillable which I’ve forgotten on every project.

That should look something like this…

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'affiliate_id', 'referred_by',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

Cool. Now what? Now we need to generate that new affiliate_id when someone registers on the site. You probably have a file called RegisterController that make:auth scaffolds for you. In there should be a create method which you’ll want to change to auto generate an affiliate code for that user.

protected function create(array $data) {
    return User::create([
        'email' => $data['email'],
        'password' => bcrypt($data['password']),
        'affiliate_id' => str_random(10),
    ]);
}

For brevity, we’ve just updated this to generate a random string for the affiliate_id field. This has a unique database constraint but you should probably do layer of validation here. If it was me I’d built an affiliate id helper function to generate a suitably unique string here that doesn’t toss a DB constraint error in your user’s faces. Just sayin.

Alright now we are rolling – now how do we track when someone uses your code to register? Here’s where it starts getting fun. We want to give the referrer credit when someone uses their unique URL in registration. First off – we need to tell them what URL to share. Lets update a Blade template.

@if(!Auth::user()->affiliate_id)
    <input type="text" readonly="readonly" 
           value="{{url('/').'/?ref='.Auth::user()->affiliate_id}}">
@endif

Now your user can copy and paste the URL they need to share to get credit.

You’ve reached the fun part. It’s time to play with Laravel Middleware.

Middleware provide a convenient mechanism for filtering HTTP requests entering your application. For example, Laravel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.

Scaffolding a Middleware is another handy artisan command.

$ php artisan make:middleware CheckReferral

The CheckReferral middleware’s job is to inspect every HTTP request to the application to see if it has the ?ref query string in the URL. In a more complex application this would allow your referrer to link a potential signup to any URL on the site (whether it be your signup page, pricing page, homepage, etc).

<?php

namespace App\Http\Middleware;

use Illuminate\Http\Response;
use Closure;

class CheckReferral
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if( $request->hasCookie('referral')) {
            return $next($request);
        }
        else {
            if( $request->query('ref') ) {
                return redirect($request->fullUrl())->withCookie(cookie()->forever('referral', $request->query('ref')));
            }
        }

        return $next($request);
    }
}

There’s an affiliate industry standard of persisting referrals in a cookie so there’s an increased chance of getting credit for the conversion. For Amazon Affiliates, the cookie expires in 24 hours. For Associate Swap it’s a forever cookie. First step in the Middleware code above is to check if the cookie already exists and move on returning the Request if it does.

If the cookie doesn’t already exist but the ref parameter exists in the query string we return the Request object as normal while attaching our referral Cookie to it.

Side Note: Building Middleware in Laravel is an excellent example of utilizing the simplicity of the Laravel Service Container – ie dependency injection.

We aren’t done yet, though. The referrer is still not getting credit. Back to the RegisterController.

<?php

namespace App\Http\Controllers\Auth;

use DB;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use Cookie;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:6|confirmed',
            'tracking_id' => 'required'
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return User
     */
    protected function create(array $data)
    {
        $referred_by = Cookie::get('referral');

        return User::create([
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
            'affiliate_id' => str_random(10),
            'referred_by'   => $referred_by
        ]);
    }
}

Now when we create the user in RegistrationController we check for that Cookie. If it has a value it gets inserted into our database. Otherwise the column gets a null value as we determined in our original migration.

Your use case might award affiliates differently so I wont go into details on how credits are applied on Associate Swap. But now you can track if a user was referred by another user. If you are using Laravel Cashier you may hook into a Stripe Webhook to pay out an affiliate in realtime after a sale is made by someone they referred. For me it was simply incrementing a counting mechanism every time they perform an action in the web application.

 

That Time I Tricked Google With Its Own Data

Once upon a time SEO was easy

Exact match keyword domain? Boom.
Pages for every keyword? Done.
Ridiculous internal link building? Easy.
Paid link building? Cheap.
Link Pyramids? Get me more shared hosting.
Link Stuffing? Count me in.
Scraping? Now we’re talking.

Back in 2007, I was working on one of the first Online Reputation Management (ORM) systems out there. Like Google did for the web, I built a spider/crawl program. But in my case, it was for a very young ecosystem – social media. It was so old school I launched it via text message on twittr.com.

screen-shot-2016-10-24-at-10-14-48-pm

The essence of the app was that a brand could enter a few keywords they wanted to track and the system would aggregate results from across the social web. Those keywords would likely be your brand or product. This was before Twitter had search functionality. Before they had purchased Summize (that $15M would have been nice though). So if you were Nike, you could see how people were talking about you online. Back then, this was new.

Soon after I built the foundation for this, Google made an interesting move with their Zeitgeist. You see, for a couple years, they released a so-called Zeitgeist of the top searches on Google for the year. This eventually turned into the Google Trends that we know today. It was mostly a marketing tool for them but then they released an RSS-based API. And that’s when the lightbulb moment happened.

screen-shot-2016-10-24-at-10-12-51-pm

I then took the platform I had built to monitor brands on social media and turned its focus on Google Trends. Instead of humans entering keywords to monitor, what if it was automated from data via Google Trends? Interesting.

So I hooked up the Google Trends RSS feed to the Fresh Feeds product I had built and called it Fresh Trends. This was cool. It was kinda like Techmeme for popular topics (I went on to create many verticals like Techmeme using this product as a backend).

The final product was a WordPress website. Each Google Trend was automatically created as a category in WordPress and then I searched my Fresh Feeds platform for content based on that keyword. The system would post an excerpt from the article along with the keyword-rich title. This happened hourly via a cron job. So in the end, I had the most content available for a trending topic on Google search. As Google pumped out trending search topics, I would take that data, search my Fresh Feeds system, and post relevant content to a separate WordPress website creating a highly optimized website based on the most popular searches for that hour. Repeat.

It got indexed by Google as top results for very high traffic, trending search terms. It was shown on top of Google News results with trending topics. It was awesome. Alas, it didn’t last forever.

screen-shot-2016-10-24-at-10-27-43-pm

And that’s the story of me tricking Google (the first time) with gray-hat techniques – in 2007.

Building Public Slack Communities – Slackvite Launch

I had the urge to build and ship something since I gave Hey Kramer to the world.

Bot Slack RTM API

Something … useful.

Since I was already elbows deep in the Slack API, I decided to build a thing that lets you “launch and manage public Slack communities in 30 seconds”. I say “manage” liberally because at this point it does none of that (the ideas list is already getting long).

But, you can launch a fancy pants landing page that pulls beautiful background photos from Unsplash to gather invites for your public Slack community. So, that’s a start I suppose.

Public Slack Community Invite

Is this a new idea? No.

There are a couple excellent solutions for building a public Slack community platform. For instance if you are reading this you’ve probably already stumbled on Slackin. It’s a great solution, however I built Slackvite for those of you that get scared away by landing on a Github page. If seeing ‘.travis.yml’ and ‘app.json’ files frightens you then you might like this.

There’s also the also very capable and popular Typeform hack for creating a Slack community. But again – if code scares you it’s not for you.

With Slackvite you just 1) register 2) connect with Slack 3) select your team 4) launch your public invite landing page to the world. Here’s a demo for an Iowa State Cyclones Slack community.

Slackvite Public Slack Community Invite Landing Page

WordPress .gitignore

Updated May 2016. I wrote the original version of this in 2014 and have since change how I work, thus the update. Unfortunately there is no *right* answer.

Wondering how git fits into your WordPress dev workflow? Here’s a great little file to help you get started – WordPress gitignore. Don’t know what a gitignore file is for? Read up on the gitignore file on the git manual.

The contents of your .gitignore file will vary depending on your style of versioning your work. Some devs keep their entire WordPress sites under version control… including WordPress core and plugins/themes from the .org repo. Others only store custom plugins and themes while ignoring any code that’s not custom. I currently manage code/workflow in all these ways as it varies by project and requirements.

Basic WordPress .gitignore

However in any case, you should ALWAYS ignore the wp-config.php file and the file uploads. You shouldn’t store wp-config.php in git because it is generally unique to the environment it is in (dev, test prod) and it potentially exposes your database username and password. The file uploads (wp-content/uploads) directory should be ignore because it should be handle like content in a database. You don’t wan to store file uploads in version control for the same reason you don’t want to keep your database rows in version control. File uploads should be treated the same way and synchronized with a tool for different environments.

wp-config.php
wp-content/uploads

Other WordPress .gitignore Considerations

There are many other types of files you don’t want to clutter up your nice clean repo. There are many type of files found in the wp-content folder that you want to ignore. In most cases it’s easier to tell git what to look for (ie themes and plugins) than it is to list all the things to explicitly ignore. For instance, one will often find backups and cache stored in wp-content. These things can easily be generated and are often environment specific. These are others to often ignore…

wp-content/advanced-cache.php
wp-content/backup-db/
wp-content/backups/
wp-content/cache/
wp-content/upgrade/
wp-content/wp-cache-config.php