$9 Marketing Stack: A Step-by-Step Guide

$9 Marketing Stack

“Just tell me what to do so I can stop worrying about tools and start making money.”

When it comes to setting up a marketing stack (analytics, email, automation, etc.), you can quickly become paralyzed by choice, especially if it’s your first time.

I’ve written about our industry’s need for default marketing stacks that prescribe which components to use and how best to use them, so you can get up and running quickly, avoid common mistakes, and focus on your business.

Below is a step-by-step guide to setting up what I call the Boostrapper’s Stack. I’m going to walk you through the exact steps I used to configure this stack for my SaaS app Munchkin Report.

What’s in the stack?

Analytics

A/B Testing

Marketing Automation (opt-in forms, email marketing, automation, and CRM)

With this stack, I can:

Pretty…pretty…pretty…good.

What’s missing?

Not much, to be honest! The goal is to keep the stack pretty lean. If you’re starting off with more than this, you’re probably overdoing it.

If money were no object, I’d swap ActiveCampaign out for HubSpot in a heartbeat. Then I could drop Optimizely. Alas, AC is $9/mo for what I need and HubSpot is definitely NOT $9/mo.

This particular stack doesn’t include a nice landing page creation tool like you’ll find in HubSpot, so I simply create landing pages like I would any other page on my sales site and embed an ActiveCampaign form. Pretty easy. The cost/overhead of adding a tool like Unbounce or LeadPages to the mix outweighs the benefit, IMO.

Out-of-scope

Other super-cheap or free marketing tools you’ll catch me using for Munchkin Report, but I consider outside the scope of my core marketing stack include:

Now that I’ve described what is in the Bootstrapper’s Stack, feel free to wander off and experiment on your own. However, if you also want to know exactly how I use the stack, stick around.

Some context

Before I dive into the detailed setup steps, I think it’s important understand Munchkin Report as a business. Munchkin Report is web-based SaaS with monthly and yearly subscription revenue.

Munchkin Report

It is used by parents who want to track their child’s activities at home (perhaps with a nanny) and daycare centers / preschools that want to ditch paper tracking sheets, improve communication with families, and differentiate themselves.

Users track naps, food, diaper changes, moods, photos, etc. The information helps parents make better decisions day-to-day, spot trends in their child’s behaviors and habits, and stay in-sync with caregivers during the day.

The north star metric for Munchkin Report is the number of parents who receive an activity report per day.

The app also serves as a memory book with pictures and a milestone timeline, but that’s a secondary benefit for most people.

Now that you know the business, let’s take a look at the marketing machinery behind it.

Step 1: Create a tracking plan

The first part of the stack to setup is analytics, which is a foundational element. But before you install analytics, it’s crucial to think through what you REALLY need to track. I highly recommend that you go through this exercise.

Here’s the outline for Munchkin Report’s tracking plan.

Our core lifecycle events are:

Our second-tier lifecycle events are:

We want to track the following key pages:

We will identify users on:

Step 2: Setup Segment

Segment is only the best invention EVER.

Install Segment and it becomes the One True Interface to all your third-party apps. You send data to Segment and it takes care of mapping and routing that data to the apps you enable.

It simplifies your setup, makes it trivial to enable new apps, and it’s more efficient. It also a cool replay feature (business plan only) which lets you send HISTORICAL data to a newly-enabled tools.

Virtually all of our setup tasks for analytics and tracking will be done via Segment.

To start, create a free account and start a new project for your app.

2.1 Install the JavaScript Library

On the Setup Project page, you’ll find both your API Write Key and the analytics.js code snippet to include on your sales site.

Segment - Project Settings

Make sure you add your API Write Key to the default code snippet.

Segment - analytics.js

Add the code to your website’s header template so that it loads on every page. You should start seeing pageview events in your live debugger now.

Add it to your web app, too!

If you also want to make client-side tracking calls from your web app (and you should), you’ll need to install the analytics.js library in your application source code as well.

For Munchkin Report, which is is a Rails app, I created a file called /app/vendor/assets/javascripts/analytics.js that contains the same snippet of code I used on my sales site.

Tip: to ensure that this library is loaded on all pages of the app, validate that your application.js file contains the following line:

# /app/assets/javascripts/application.js

//= require_tree ../../../vendor/assets/javascripts/

Reference: https://segment.com/docs/libraries/analytics.js/quickstart/

2.2 Install a server-side library (optional)

If you want to make server-side HTTP calls to Segment, you’ll want a server-side library. Luckily, Segment has a whole bunch of ’em.

They also have a great article which answers the question: “When should I use client vs. server tracking?

I’m using the Ruby library, which I installed by adding a gem to my Gemfile:

# /Gemfile

gem 'analytics-ruby', '~> 2.0.0', :require => 'segment/analytics'

And then I created an initializer:

# /config/initializers/analytics_ruby.rb

Analytics = Segment::Analytics.new({
  write_key: 'YOUR_WRITE_KEY',
  on_error: Proc.new { |status, msg| print msg }
})

Done!

Reference: https://segment.com/docs/libraries/ruby/quickstart/

2.3 Install the WordPress plugin (optional)

If you have a blog on a separate platform or codebase, you’ll want to add analytics.js there, too.

While munchkinreport.com is a static HTML site, the blog runs on its own WordPress instance. Segment has a handy WordPress plugin that automagically tracks everything you’d want to track. You can install it by searching for “segment” in the WordPress plugin search tool.

Segment - WordPress Plugin

Reference: https://segment.com/docs/platforms/wordpress/

2.3 Implement the tracking plan

Now we need to drop in bits of code to tell Segment when key things happen, according to our tracking plan.

Page Tracking

I have 3 key pages I want to track, so on each of them I’ll add a one-liner to indicate that the visitor is viewing a key page.

Add a page call to my Tour page:

<!-- /pricing/index.html -->

analytics.page('Tour');

Add a page call to my Pricing page:

<!-- /pricing/index.html -->

analytics.page('Pricing');

The signup page is hosted in the app itself, not the sales site, so I add the page call to the JavaScript on the signup page in my Rails project:

# /app/views/devise/registrations/new.html.erb

<%= javascript_tag do %>
  analytics.page('Sign Up');
<% end %>

Identifying Users

Segment automatically cookies every new visitor to your site with an anonymous ID. When a user becomes known–via opt-in or sign up/in–calling identify links that identity to all the previously anonymous activity you’ve collected.

Managing identities can be a major source of pain with analytics tools, so pay close attention to this.

There are 2 points in my funnel where I’m going to identify a user: when they’re logged into the app and after they download a resource.

Here’s what the identify code looks like when someone lands on any of my app’s internal pages after logging in:

# /app/views/shared/_analytics.html.erb

<script>
<% if current_user %>
  // Segment
  analytics.identify('<%= current_user.id %>', {
    name: '<%= current_user.full_name %>',
    email: '<%= current_user.email %>'
  });
  ...
<% end %>
</script>

To see how I’m identifying users after a resource download, read the section below titled Downloaded Resource.

Lifecycle Event Tracking

The Signed Up event is called from the app as well:

# /app/views/shared/_analytics.html.erb

<script>
...
<% if params[:init] == "true" %>
  analytics.alias('<%= current_user.id %>');
  analytics.track('Signed Up', {
    userLogin: '<%= current_user.email %>',
    type: 'organic',
    accountId: '<%= current_user.account.id %>'
  });
  ...
<% end %>
</script>

The Logged Activity event triggers whenever a user (parent or teacher) adds activitiy for a child. Since I have multiple controllers for each activity type, I have a concern that includes all of the shared methods for those controllers.

# /app/controllers/concerns/eventable_controller.rb

def track_logged_activity(activity_type)
  Analytics.track(
    user_id: current_user.id,
    event: 'Logged Activity',
    properties: {
      type: activity_type,
      accountId: current_user.account.id
    })
end

Then in each of the 5 activity controllers, I add a method call to track_activity within the create action:

# /app/controllers/meals_controller.rb

def create
  @meal = @munchkin.meals.build(meal_params)

  respond_to do |format|
    if @meal.save
      ...
      # Send "Logged Activity" event to Segment
      track_logged_activity controller_name.classify.singularize
      ...
    end
  end
end

Downloaded Resource

For resource downloads I use a single modal form at http://www.munchkinreport.com/resources to collect leads. When someone clicks the submit button, I have some JavaScript that pauses the submission, assigns some hidden field values, and then reports to Segment before the request transitions to the “thank you” page.

<!-- /resources/index.html -->

<!-- Set the right resource name in the modal -->
<script>
$(function () {
  // Set resource name when the modal form opens up
  $('a.resource-item').click( function () {
    var resourceName = $(this).attr('data-resource-name');
    setResourceName(resourceName);
    return true
  });

  // Grab a reference to our form
  form = $('form');
  // Run the function when the form submits
  form.on('submit', function(e) {
    // Stop the form from submitting... for now
    e.preventDefault();
    // Get the email address the user has entered
    email = $(e.target).find('[name=email]').val();
    name = $(e.target).find('[id=resourceName]').val();
    // Identify this visitor using their email address as a distinct ID
    // and as a new property
    analytics.alias(email);
    analytics.identify(email, {
      email: email
    });
    analytics.track("Downloaded Resource", {
      resourceName: name
    });
    // Submit the form now that all our analytics stuff is done
    $(e.target).unbind('submit').trigger('submit');
  });

});

function setResourceName(resourceName) {
  $('form input#resourceName').val(resourceName);
  $('#resourceModal #myModalLabel').text(resourceName);
}
</script>

Note: if I had a landing page for each resource, I would use Segment’s trackForm helper. But because I have to dynamically set and read the hidden resourceName field on-the-fly, I had to roll my own.

Invited User

# /app/controllers/parents_controller.rb

def track_invited_user
  Analytics.track(
    user_id: current_user.id,
    event: 'Invited User',
    properties: {
      inviteeEmail: @parent.email,
      accountId: current_user.account.id
    })
end

Added Munchkin

# /app/controllers/munchkins_controller.rb

def track_added_munchkin
  Analytics.track(
    user_id: current_user.id,
    event: 'Added Munchkin',
    properties: {
      creatorType: current_user.type,
      accountId: current_user.account.id
    })
end

Sent Daily Report

# /app/controllers/munchkins_controller.rb

def track_sent_daily_report(to_list)
  Analytics.track(
    user_id: current_user.id,
    event: 'Sent Daily Report',
    properties: {
      recipientList: to_list,
      munchkinId: @munchkin.id,
      reportDate: @selected_date.strftime('%Y-%m-%d'),
      accountId: current_user.account.id
    })
end

2.4 Enable integrations

Now I’m going to enable 3 integrations:

  1. Google Analytics
  2. Mixpanel
  3. Optimizely

At this point, I’ve created accounts for the tools listed above, but I haven’t touched them yet. Instead of “installing” these tools, I’m simply going to enable them in Segment.

Segment - Integrations

The foundation I laid with Segment’s analytics.js and the track, page, and identify calls will start paying off.

Ah, the beauty of Segment.

Step 3: Google Analytics reports

There’s not much to configure in GA since Segment handles your tracking code and will automatically log GA events for each track call:

Google Analytics Events created by Segment

3.1 Configure Goals

Goals are important to setup because they add vital context to many built-in GA reports. For example, it’s nice to know which channels generate the most traffic to your site (via your Channels report), but it’s critical to know which traffic sources convert the best. You can’t do that without Goals.

Channels report (now with Goals!)

Since I already have my key conversion events coming in via Segment, it takes no time to setup Goals based on them.

I’m not going to setup a Goal for every event that I track. That’d be overkill. I’m only going to set them up for the following core lifecycle events (for now):

Just go to Admin and under your View section, click on Goals and create a new goal based on an Event:

Google Analytics goals based on events

3.2 Enable Demographic and Interest Reporting

I like to turn on the Demographic and Interest Reporting feature (via Segment, which will handle the GA tracking code update for you automatically):

Demographics Reporting

And then enable those report features in my GA property settings:

Demographics Reports setting in Google Analytics

Because who doesn’t want to know their audience demographics and interests?! Age, gender, and categorical interest data will now be visible under the Audience tab.

Other things I look at frequently in Google Analytics:

I like Portent’s Perfect GA Dashboard.

Step 4: Mixpanel reports

I use Mixpanel for two core features: funnels and cohorts.

Mixpanel has already been receiving events from Segment, so I can create funnels on-the-fly:

Resource download funnel

I’ve created funnels to visualize the steps people take towards critical events:

And so on.

The other report I like to use in Mixpanel is the Retention report, which tells me which cohorts of users are sticking around and continually using my app. You can segment by any property to get a really granular view of things. For example, you can easily get a breakdown of retention rates for each subscription plan.

I haven’t really used the people reporting and notification features in Mixpanel yet, but they look pretty cool.

Step 5: Optimizely

Create an Optimizely account, grab their JavaScript snippet and add it to your sales site in the <head> tag before any other JavaScript tags. That’s all you have to do to start A/B testing pages on your site. The interface is super intuitive, so go ahead and create a simple test, perhaps with your homepage headline.

I have a confession to make: I lied to you.

You have to install Optimizely’s JS code OUTSIDE of Segment. Why? Well, because Optimizely does some crazy page modification magic that stipulates that it loads synchronously and before anything else. This is the only exception, I promise!

You should STILL enable the Optimizely integration in Segment because they’ll send custom events through to Optimizely to help you measure A/B test conversions and pass the A/B test experiment variations through to Mixpanel for better analysis.

Also, this integration isn’t supported on the Startup plan, but if this specific feature (i.e., sending the experiment info to Mixpanel) is important to you, Optimizely and Mixpanel have a free integration of their own that does the same thing.

Custom goals

You probably should track your Optimizely experiment against a custom goal (versus just looking at engagement)–probably one of the lifecycle events we defined earlier like Signed Up or Downloaded Resource.

Make the name of the Optimizely goal event exactly match the corresponding Segment event name.

Optimizely Goals

Then trigger the Optimizely event right after the Segment event in your codebase:

# /app/views/shared/_analytics.html.erb

<script>
<% if current_user %>
  ...
  <% if params[:init] == "true" %>
    // Segment
    analytics.track('Signed Up', {
      userLogin: '<%= current_user.email %>',
      type: 'organic',
      plan: '<%= current_user.account.plan %>',
      accountId: '<%= current_user.account.id %>',
      accountSubdomain: '<%= current_user.account.subdomain %>'
    });

    // Optimizely
    window['optimizely'] = window['optimizely'] || [];
    window.optimizely.push(["trackEvent", "Signed Up"]);

  <% end %>
<% end %>
</script>

Step 6: ActiveCampaign

ActiveCampaign is a very solid, well-designed marketing automation tool. They’re not as comprehensive as HubSpot, but they honestly have no business away so much functionality for $9/month.

Right now I use ActiveCampaign for the following things:

  1. Capture leads via forms (and the API)
  2. Send email nurture sequences (one for leads, one for trials)

Capturing leads via forms

I created a form for my Leads list, configured the fields, and placed the emebed code on the resources page on my site.

I don’t like ActiveCampaign’s double opt-in process, so I did some additional customization:

  1. Un-check the box under Opt-in confirmation email to disable double opt-in.

ActiveCampaign - Disable Double Opt-in

  1. Click on Form settings and edit the subscription confirmation page. I redirect submissions to a plain old HTML page that I control completely.

ActiveCampaign - Custom URL

When a visitor fills out the Resource Form, they are added to the Leads list and redirected to my generic resource “thank you” page.

An automation is triggered which emails them the resource they asked for and tags them with “Resource: {resourceName}”. If they’re a brand new lead, they are also enrolled in my content funnel automation and will receive all our best content over time.

Email nurture automation

Capturing leads via the API when a trial is started

My trial signup form posts to the Rails app, not ActiveCampaign, but I DO want these users in my marketing database so I can send them an onboarding email sequence, tag them, etc.

So, after a new user signs up, organically or by accepting an invite from another user, I sync them to ActiveCampaign via the API.

There is a third-party Ruby wrapper for the ActiveCampaign API, but it didn’t work for me and I didn’t feel like writing my own, so I used Zapier instead.

Here’s the method I wrote on the User class:

# /app/models/user.rb

def update_marketing_contact(signup_type, tags)
  params = {
    email: self.email,
    first_name: self.first_name,
    last_name: self.last_name,
    subdomain: self.account.subdomain,
    plan: self.account.plan,
    signup_type: signup_type,
    tags: tags
  }
  Zapier.zap(:contact_sync, params)
rescue => error
  logger.error error
end

After a new trial user is “zapped” into ActiveCampaign from my app, they’ll enter the onboarding email sequence which sends helpful emails about how to use them app.

Wow. That was A LOT, huh?

This just goes to show that laying a solid foundation for analytics and marketing automation takes foresight and planning, some technical chops, an analytical mindset, and some trial and error. And my app is small potatoes compared to many of the businesses you guys are dealing with.

I hope this guide can serve as a step-by-step implementation plan for those that want to use this precise stack, and for those that want to mix it up, maybe you can use my setup as a loose template.

Feeling overwhelmed? No worries, I’ve got your back.

Analytics X-Ray

Featured image credit (cc): http://www.flickr.com/photos/basheertome/13739968364