Noom Friendly Instant Pot Split Lentil Soup

Another green soup just in time for St. Paddy’s Day. I am not actually a big St. Paddy’s Day fan. It just happens that my favorite soups are green. This weekend, I am at my parents’s place so I decided to make this Instant Pot lentil one for them.

This soup is great because it is a great source of veggie protein while at the same time being very Noom friendly at only 240 calories per serving. My recipe makes about 4. On top of that, it’s a throw it all in the Instant Pot and click go recipe. It can’t get any easier than that.

Thick Instant Pot Vegan Split Pea Soup

I’ve been using this pea soup recipe for a while now and it is a bit of a favorite with the kids. It is actually less of a soup and more of a mash because of how thick it is. I’m wondering if that is what makes it so appealing to them. Anyhow, I am just happy it is another way I figured out how to get more veggies in them. It is super quick and easy to make being a dump it all in the Instant Pot and click go recipe. With St. Paddy’s day just around the corner and this soup being fairly green, I figured it was a proper time to post about it.

Calling the Shopify Storefront API from a WordPress Plugin

Shopify and WordPress are great 3rd party hosting providers. Shopify lets you quickly stand up a retail Website and WordPress is solid for putting together content oriented ones, like this one. So of course from time to time, if you use both, you will want to make a deeper integration between the two. In this post, I am going to cover the basics for exactly that; setting up a WordPress plugin that interacts with the Shopify Storefront API; this plugin will let you make custom retail experiences on your WordPress site that go well beyond what liquid, Shopify’s template engine, can support while at the same time still leveraging Shopify for inventory and order management.

A WordPress plugin is where you want to do this kind of integration work. It is possible to bake this functionality directly into a theme, though, that doesn’t make a lot of sense in my opinion. It is just easier to encapsulate these kinds of integrations as plugins and then you can use them in any WordPress theme you choose.

Step 1: Setup your dev environment

To follow along here, you will need a Shopify development site. You can create as many of those for free. You can add some products to its catalog if you want but that is entirely optional for this tutorial. Doing so, however, will let you better explore the functionality of the Storefront API so it isn’t a bad idea once you have completed the steps here.

Next you should setup a development instance of WordPress. You can do that remotely with a hosting provider or you can setup WordPress locally. Local setup is my preference; you can just do things a lot faster locally. I use a Mac for development so my setup involves a MAMP environment. WordPress has instructions for that here. It is largely straightforward. Once you have MAMP up and running, download and install WordPress in the directory that your MAMP environment uses for hosting content. This tutorial is written for version 5.3.

Step 2: Create a WordPress plugin

WordPress plugin development is fortunately very easy. It can certainly get complex in a hurry, though, that is more related to the functionality that you may want to incorporate. Plugins really just boil down to some PHP with a touch of organization. The documentation is here but you can look at it later. We’re going to make a simple plugin called hello-tech. I would typically go for the standard hello-world but someone has already published a plugin with that name to the WordPress marketplace.

In your WordPress install directory, create a new plugin directory:
wp-content/plugins/hello-tech

The directory wp-content/plugins should already exist. If it doesn’t, you might be looking in the wrong spot in your WordPress install or there is something wrong with your install. hello-tech is the name of your plugin. In this new directory, create a new file call hello-tech.php. It is a good practice to name your PHP file after your plugin. In order for this file to be recognized as a plugin it needs to start with a WordPress plugin header. This header can have information about the plugin, its maintainers, and its license. However, for this tutorial let’s keep it simple and use with the following:

<?php
/**
 * Plugin Name: Hello Tech
 */

That is all your plugin needs for you to see it in the WordPress admin dashboard. It doesn’t have any functionality but you can still activate it; do that now by clicking on activate. Now we’re ready to add some functionality. Update hello-tech.php to be the following:

<?php
/**
 * Plugin Name: Hello Tech
 */
function helloTech() {
    return '<strong>hello tech</strong>'; 
}
add_shortcode('hello-tech', 'helloTech');

The above snippet does 2 things. First it creates a function called helloTech that returns an HTML snippet. Second it registers a new shortcode, hello-tech, that uses it. Shortcodes are a quick way to incorporate plugin functionality into a WordPress site. If you add [hello-tech] to the contents of any page on your site, you will see hello tech appear. There are other methods for triggering plugin functionality but we are going to stick with shortcodes for now.

Step 3: Create a Storefront Access token

You need an access token to send Storefront requests. The steps for generating a token are here.

Essentially you need to create a new private app that at least has read access to some of your store data. For this tutorial, I set the private app name to be Hello Tech. You don’t need to worry about Admin API access; you can disable all of those permissions. At the bottom of the private app settings page is where you will find the Storefront API options. Don’t forget to enable access.

Step 4: Send a Storefront API request

With the access token you can now issue Storefront API requests. Update your plugin to look like the following. Swap STORE_NAME and ACCESS_TOKEN for your Shopify store name and Storefront access token:

<?php
/**
 * Plugin Name: Hello Tech
 */
function helloTech() {
    $store_name = 'STORE_NAME';
    $access_token = 'ACCESS_TOKEN';
    $query = <<<'GRAPHQL'
    query {
        shop {
            name
            primaryDomain {
                url
                host
            }
        }
    }
    GRAPHQL;
    
    $storefront_response = wp_remote_post(
        'https://'.$store_name.'.myshopify.com/api/2019-07/graphql.json', 
        array(
            'method' => 'POST',
            'timeout' => 30,
            'blocking' => true,
            'httpversion' => '1.1',
            'headers' => array(
                'Accept' => 'application/json',
                'Content-Type' => 'application/json; charset=utf-8', 
                'X-Shopify-Storefront-Access-Token' => $access_token
            ),
            'body' => json_encode(['query' => $query, 'variables' => '']),
        )
    );

    $storefront_json = json_decode($storefront_response['body'], true);
    return '<strong>hello tech</strong><br /><br />'. 
        '<code>'.$storefront_response['body'].'</code><br /><br />'.
        '<code>'.$storefront_json['data']['shop']['name'].'</code>'; 
}
add_shortcode('hello-tech', 'helloTech');

The above plugin issues a request to get your store name, URl, and host. It is basically a hello world Storefront query that you can use to make sure your access token and plugin are working. For more information on the Storefront API and to develop queries that meet your use cases, have a look at the reference documentation.

Step 5: Move the Storefront API key into plugin settings

Our plugin now issues Storefront API requests and displays the results on a page via shortcode. Our store name and access token, however, are hard coded into the plugin. Let’s do a final touch and move those configurations to a custom settings page.

First, let’s create a separate PHP file to contain our custom settings page. Update hello-tech.php to have these 2 lines at the top to require a new file called hello-tech-settings.php:

define( 'HELLO_TECH__PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

require_once( HELLO_TECH__PLUGIN_DIR . 'hello-tech-settings.php' );

In the hello-tech plugin directory, create the file hello-tech-settings.php. Forewarning, these custom settings pages are a little verbose. The WordPress Codex provides a decent tutorial on them and you can also look at the reference documentation. They do suggest using arrays to store key/values if you are dealing with a lot of options but since we only have two, we’re going to skip that optimization. Set the contents of hello-tech-settings.php to the following:

<?php

function hello_tech_plugin_admin_init(){
    register_setting( 'hello_tech_options', 'hello_tech_shopify_store_name', 'noop_validator' );
    register_setting( 'hello_tech_options', 'hello_tech_storefront_access_token', 'noop_validator' );
    add_settings_section('hello_tech_main', 'Shopify Settings', 'hello_tech_shopify_section_text', 'hello_tech_plugin');
    add_settings_field('hello_tech_shopify_store_name', 'Shopify Store Name', 'hello_tech_shopify_store_name_setting', 'hello_tech_plugin', 'hello_tech_main');
    add_settings_field('hello_tech_storefront_access_token', 'Storefront Access Token', 'hello_tech_storefront_access_token_setting', 'hello_tech_plugin', 'hello_tech_main');
}
add_action('admin_init', 'hello_tech_plugin_admin_init');

function hello_tech_shopify_section_text() {
    echo '<p>Please set the Shopify store name and Storefront access token.</p>';
}

function hello_tech_shopify_store_name_setting() {
    $shopify_store_name = get_option('hello_tech_shopify_store_name');
    echo "<input id='hello_tech_shopify_store_name' name='hello_tech_shopify_store_name' size='40' type='text' value='{$shopify_store_name}' />";
}

function hello_tech_storefront_access_token_setting() {
    $storefront_access_token = get_option('hello_tech_storefront_access_token');
    echo "<input id='hello_tech_storefront_access_token' name='hello_tech_storefront_access_token' size='40' type='text' value='{$storefront_access_token}' />";
}

function noop_validator($input) {
    return $input;
}

function hello_tech_plugin_menu() {
	add_options_page('Hello Tech Options', 'Hello Tech', 'manage_options', 'hello-tech-settings', 'hello_tech_options');
}
add_action('admin_menu', 'hello_tech_plugin_menu');

function hello_tech_options() {
	if ( !current_user_can('manage_options') )  {
		wp_die(__('You do not have sufficient permissions to access this page.'));
    }
?>
	<div class="wrap">
        <h2>Hello Tech</h2>
        <form action="options.php" method="post">
<?php
    settings_fields('hello_tech_options');
    do_settings_sections('hello_tech_plugin');
 ?>
            <input name="Submit" type="submit" value="<?php esc_attr_e('Save Changes') ?>" />
        </form>
	</div>
<?php
}

If you look in Settings in the admin dashboard, you should see a new entry for “Hello Tech”. Click it and you will see a basic settings page with a two field form asking for your Shopify store name and Storefront access token. Enter in those values and click “Save Changes”.

Next update your plugin to use those two options with the get_option function instead of the hard coded values. hello-tech.php should look like the following:

<?php
/**
 * Plugin Name: Hello Tech
 */

define( 'HELLO_TECH__PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

require_once( HELLO_TECH__PLUGIN_DIR . 'hello-tech-settings.php' );

function helloTech() {
    $store_name = get_option('hello_tech_shopify_store_name');
    $access_token = get_option('hello_tech_storefront_access_token');
    $query = <<<'GRAPHQL'
    query {
        shop {
            name
            primaryDomain {
                url
                host
            }
        }
    }
    GRAPHQL;
    
    $storefront_response = wp_remote_post(
        'https://'.$store_name.'.myshopify.com/api/2019-07/graphql.json', 
        array(
            'method' => 'POST',
            'timeout' => 30,
            'blocking' => true,
            'httpversion' => '1.1',
            'headers' => array(
                'Accept' => 'application/json',
                'Content-Type' => 'application/json; charset=utf-8', 
                'X-Shopify-Storefront-Access-Token' => $access_token
            ),
            'body' => json_encode(['query' => $query, 'variables' => '']),
        )
    );

    $storefront_json = json_decode($storefront_response['body'], true);
    return '<strong>hello tech</strong><br /><br />'. 
        '<code>'.$storefront_response['body'].'</code><br /><br />'.
        '<code>'.$storefront_json['data']['shop']['name'].'</code>'; 
}
add_shortcode('hello-tech', 'helloTech');

Closing thoughts

This tutorial walks you through creating a WordPress plugin that makes calls to Shopify’s Storefront API. You can also use it as a template for making other plugins that issue REST calls to a 3rd party API with relatively little effort. Once you can make that all important request and get back a response, then all you have to do is work it into a reasonable UX for your users.

New PB 12 minute treadmill run

Well, I guess I’ve been on a tear lately at Orangetheory. Yesterday they had a 12 minute treadmill benchmark run and I kind of crushed my previous best by almost .1 miles. I managed to do 1.94 miles with a negative split which I think means 2 miles is well within reach for me.

I know that treadmill running is definitely not outdoor running but I’m still pretty happy about this PB. One of the things I have noticed about my outdoor running is that while I am certainly faster these days, I do need to constantly remind myself to go faster. The nice thing about the treadmill is that you can set your pace and then just not think about it anymore. Outside, you really are on your own with your pace. There have been times where I start rocking out to a song on my phone and suddenly I’ve missed up my last mile going way slower than I should have.

My new work mug

The other week I got my February coffee club shipment from Nocking Point. This time in addition to my light and dark roast beans, I got this mug. I’m kind of digging it and it has taken over as the primary one that I use at work. My old mug is this NPR one I got about 10 years ago while I was still living in New York. I guess I was about due for a change.

They also switched up the coffee roaster to Rockabilly, another small batch operation. I’m working on their light roast and it is great. So far I’ve been super happy with this coffee club.

200, 000 Orangetheory calories burned

Last night, I hit a bit of a milestone with my Orangetheory workouts. I broke the 200k calories burned mark for all time. I’ve been going to Orangetheory for over two years now to augment my running. I have found it to be a great way to get my core strengthening and speed training done. The 200k number is a little too big for me to quantify on its own beyond just thinking that I have burned a ton of calories. So I’ve googled some cheat day foods to make it more real. Here’s a small list of things I’ve come up with so far. Grapes clearly aren’t a cheat day food but I put them on there to give some perspective to the others. Junk food is junk:

  • 76 large Dominos Hawaiian pizzas
  • 99 plates of Applebee’s neighborhood nachos with beef
  • 175 Costco cheeseburgers
  • 220 Chipotle carnitas burritos with sour cream and cheese
  • 315 bottles of red wine
  • 362 Costco hot dogs
  • 526 Starbucks grande pumpkin spice lattes
  • 2, 198 Oatmeal chocolate chip cookies
  • 34, 483 Cadbury mini eggs
  • 58, 824 M&Ms
  • 100, 000 grapes

Marshmallow Fondant Is A Thing

We recently celebrated one of my kid’s birthdays and I decided to try to up my baking game. I discovered that you can easily make fondant out of marshmallows which is pretty mind blowing for me. Fondant has always been one of those things that I’ve steered away from since you could only buy small packets of it at the store and making it from scratch was a fairly involved process. This marshmallow based recipe I have essentially only needs marshmallows, powdered sugar, and a microwave.

Working with fondant is a lot of fun. It is really just edible Play-Doh. I made these froggie cupcakes and I don’t think they are that bad for a first attempt. In hindsight, I’ve realized that working with a cake as opposed to cupcakes would have been easier. This is a case where bigger is simpler. I’m definitely going to be messing with fondant for the next birthday.

Nocking Point’s Origins Pinot Noir

Well, I have had a chance to try out that collab red wine that Jason Momoa put out with Nocking Point, Dirtbag, and it is fantastic. I am not sure what I was expecting from a red wine by Aquaman but it certainly was a very happy surprise for me. So much so, I’ve already ordered two more bottles.

In addition to the Dirtbag, I’ve also bought two bottles of their Pinot Noir, Origins. Stephen Amell apparently loves Pinot Noir and this is their homage vintage to people and places that have helped them get to where they are today. It’s the second time they have made it so my expectations are a little higher than what I thought I was in for with Dirtbag.

Applying Mixpanel to Shopify Stores

Mixpanel is probably my favorite off the shelf analytics platform. More precisely, it is a behavioral analytics platform which means it can help you understand how your users are behaving when they are using your Website or mobile app. With very little effort you can understand how users progress through funnels that you define; what your retention rate looks like; and what kind of user cohorts you might have. In short, Mixpanel gives you the fine grained understanding of your users that you absolutely need at any stage of your business.

In this tutorial, I am going to cover step by step how to update your Shopify theme to install the Mixpanel JS library and start tracking page views, cart adds, and orders. I thought about breaking this post into two parts but when you want to instrument your Shopify store, it isn’t really something you want to do partially. You can do all your work in a Shopify development store and Mixpanel development project. When you are ready to promote it to PROD, it should be relatively painless.

Step 1 – Create a dev Mixpanel project

You can actually create as many projects as you want in Mixpanel for free. For this tutorial, either create a brand new one or reuse an exist dev one. In the project settings, under Access Keys, you will see your project’s token. We need that token to complete Step 3.

Step 2 – Setup your local dev environment

Make sure you have Theme Kit installed and you are using a Shopify development store. If you are brand new to Shopify template development, have a look at Step 1 in this earlier tutorial I wrote. It discusses this setup in more detail.

Step 3 – Add the Mixpanel project token as a theme setting

Add the following to config/settings_schema.json. This file contains a JSON array of objects representing different settings in your theme. I recommend adding the snippet below as the last element of this array. You will need to add a comma to the previous object in order for the JSON to continue to be a valid array.

  {
    "name": "Mixpanel",
    "settings": [
      {
        "type": "text",
        "id": "mixpanel_token",
        "label": "Token",
        "default": "Your Token"
      }
    ]
  }

In the admin dashboard, when you select to customize your theme, you can view your theme settings from the left panel in the editor. When you updated the array in config/settings_schema.json, you added Mixpanel as an entry in this list. Copy the Mixpanel project token from Step 1 into the Mixpanel token settings field.

Step 4 – Install the Mixpanel JS library

The Mixpanel documentation for installing their JS library provides you with a JS snippet where you have to replace on the last line YOUR TOKEN with the token from your project. We want to use our Mixpanel token setting that we created in the previous step so we are going to swap in {{settings.mixpanel_token}}. That removes the hard coded token so we can have Mixpanel tracking work for any store that is using your theme. Most commonly that is going to be different environments but if you are a theme developer, you can now support Mixpanel. The snippet should look like the following:

  <!-- start Mixpanel -->
  <script type="text/javascript">(function(c,a){if(!a.__SV){var b=window;try{var d,m,j,k=b.location,f=k.hash;d=function(a,b){return(m=a.match(RegExp(b+"=([^&]*)")))?m[1]:null};f&&d(f,"state")&&(j=JSON.parse(decodeURIComponent(d(f,"state"))),"mpeditor"===j.action&&(b.sessionStorage.setItem("_mpcehash",f),history.replaceState(j.desiredHash||"",c.title,k.pathname+k.search)))}catch(n){}var l,h;window.mixpanel=a;a._i=[];a.init=function(b,d,g){function c(b,i){var a=i.split(".");2==a.length&&(b=b[a[0]],i=a[1]);b[i]=function(){b.push([i].concat(Array.prototype.slice.call(arguments,
0)))}}var e=a;"undefined"!==typeof g?e=a[g]=[]:g="mixpanel";e.people=e.people||[];e.toString=function(b){var a="mixpanel";"mixpanel"!==g&&(a+="."+g);b||(a+=" (stub)");return a};e.people.toString=function(){return e.toString(1)+".people (stub)"};l="disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");
for(h=0;h<l.length;h++)c(e,l[h]);var f="set set_once union unset remove delete".split(" ");e.get_group=function(){function a(c){b[c]=function(){call2_args=arguments;call2=[c].concat(Array.prototype.slice.call(call2_args,0));e.push([d,call2])}}for(var b={},d=["get_group"].concat(Array.prototype.slice.call(arguments,0)),c=0;c<f.length;c++)a(f[c]);return b};a._i.push([b,d,g])};a.__SV=1.2;b=c.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?
MIXPANEL_CUSTOM_LIB_URL:"file:"===c.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";d=c.getElementsByTagName("script")[0];d.parentNode.insertBefore(b,d)}})(document,window.mixpanel||[]);
mixpanel.init("{{settings.mixpanel_token}}");</script>
  <!-- end Mixpanel -->

Place the snippet in layout/theme.liquid inside the head tag.

Step 5 – Track page views

The first four steps focused on getting things setup and installed. Now we can start doing the fun stuff. To start tracking page views, append the following snippet to layout/theme.liquid inside the body tag:

<script type="text/javascript">
if ('undefined' !== typeof mixpanel) {
  mixpanel.track('Viewed Page');
}
</script>

Now let’s give this tracking a test drive. Start browsing around your development store. You might not have much but at least hit the homepage a couple times. Now in the Mixpanel project dashboard, go to the Analysis tab and select Live view. This particular view is only really useful for development and troubleshooting. It lets you see tracking events generated on your site or app in real time. If everything has been setup correctly, you should see Viewed Page events for your development store.

Step 6 – Identify users at sign up and login

The events that you track become a lot more valuable if you can attach them to actual user profiles. The opportunities to do that are at sign up and login. To handle those scenarios, we are going to need to leverage cookies. That is because unfortunately, Shopify is a little unpolished when it comes to detecting when users sign up or sign in. Regardless, I like to leverage js-cookie to manage cookies. Add the following snippet to layout/theme.liquid inside the head tag. This snippet also includes jQuery which I am using to simplify my code. If your theme is already using it, then only include js-cookie:

<script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js"></script>

In templates/customers/register.liquid add the following script that will drop a cookie with the user email when the registration form is submitted:

<script>
$('#create_customer').submit(function() {
  Cookies.set('registered_email', $('#email').val());
});
</script>

We can now look for this cookie in layout/theme.liquid in the Mixpanel script tag that we added in Step 5.

  // If the user is logged in
  {% if customer %}
  // User just signed up
  if ("{{customer.email}}" === Cookies.get('registered_email')) {
    mixpanel.alias("{{customer.email}}");
    Cookies.remove('registered_email');
  } else {
    mixpanel.identify("{{customer.email}}");
  }

  mixpanel.people.set_once({ 
    "$name": "{{customer.first_name}} " + "{{customer.last_name}}", 
    "$email": "{{customer.email}}" 
  });
  {% endif %}

Step 7 – Order Processing

Shopify generates its own order processing pages which has its strengths and weaknesses. One of those drawbacks comes in having to replicate the tracking logic for those pages regardless if you are using Mixpanel or not. It isn’t the worse thing in the world but it is definitely something you need to address when instrumenting your store.

From your store’s admin dashboard, go to the settings page and click checkout. On the checkout settings page, scroll down to the order processing section. There you will see a text area called additional scripts. Copy and paste the snippet below in there. Don’t forget to replace YOUR TOKEN with your Mixpanel project token.

  <!-- start Mixpanel -->
  <script type="text/javascript">(function(c,a){if(!a.__SV){var b=window;try{var d,m,j,k=b.location,f=k.hash;d=function(a,b){return(m=a.match(RegExp(b+"=([^&]*)")))?m[1]:null};f&&d(f,"state")&&(j=JSON.parse(decodeURIComponent(d(f,"state"))),"mpeditor"===j.action&&(b.sessionStorage.setItem("_mpcehash",f),history.replaceState(j.desiredHash||"",c.title,k.pathname+k.search)))}catch(n){}var l,h;window.mixpanel=a;a._i=[];a.init=function(b,d,g){function c(b,i){var a=i.split(".");2==a.length&&(b=b[a[0]],i=a[1]);b[i]=function(){b.push([i].concat(Array.prototype.slice.call(arguments,
0)))}}var e=a;"undefined"!==typeof g?e=a[g]=[]:g="mixpanel";e.people=e.people||[];e.toString=function(b){var a="mixpanel";"mixpanel"!==g&&(a+="."+g);b||(a+=" (stub)");return a};e.people.toString=function(){return e.toString(1)+".people (stub)"};l="disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");
for(h=0;h<l.length;h++)c(e,l[h]);var f="set set_once union unset remove delete".split(" ");e.get_group=function(){function a(c){b[c]=function(){call2_args=arguments;call2=[c].concat(Array.prototype.slice.call(call2_args,0));e.push([d,call2])}}for(var b={},d=["get_group"].concat(Array.prototype.slice.call(arguments,0)),c=0;c<f.length;c++)a(f[c]);return b};a._i.push([b,d,g])};a.__SV=1.2;b=c.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?
MIXPANEL_CUSTOM_LIB_URL:"file:"===c.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";d=c.getElementsByTagName("script")[0];d.parentNode.insertBefore(b,d)}})(document,window.mixpanel||[]);
mixpanel.init("YOUR TOKEN");</script>
  <!-- end Mixpanel -->

<script type="text/javascript">
mixpanel.track('Viewed Order Status');

mixpanel.identify("{{customer.email}}");
mixpanel.people.set_once({ 
    "$name": "{{customer.first_name}} " + "{{customer.last_name}}", 
    "$email": "{{customer.email}}" 
  });
</script>

In the snippet that I have provided, in addition to tracking the “Viewed Order Status” event, I also associate the event with the user profile identified using email. That impacts how guest checkouts are tracked since we do not have those user emails until then. It is a judgment call here since using mixpanel.identify() will orphan prior anonymous events generated by those users. In my opinion, it is worth it since your most valuable insights will come from converting customers.

To test this step, you should place some test orders. The instructions for setting up a bogus gateway in your Shopify development store are here.

Step 8 – Track orders

After Step 7, you should have all of your store page views tracked as user events in Mixpanel. At this point, you can setup funnel and retention reports in Mixpanel that will tell you how well your store is doing at converting new users into buying customers and if it is able to keep them coming back. Those insights are quite important at any stage of your store and can help you make vital UX adjustments.

In this step, we are updating our order process tracking to attribute order values to our users in Mixpanel which is helpful in defining cohorts. It might feel like we have skipped over cart adds which is an important event for our funnel reporting but the changes that we need to make are directly related to the ones you just did in Step 7. The next step looks at cart adds.

Append the line below to the last script tag in Step 7. It associates the total checkout value with the profile we have for the user in Mixpanel:

mixpanel.people.track_charge({{checkout.total_price}} / 100); 

So the additional scripts text area in your order processing section should look like the following. Take care to replace YOUR TOKEN with your Mixpanel project token:

  <!-- start Mixpanel -->
  <script type="text/javascript">(function(c,a){if(!a.__SV){var b=window;try{var d,m,j,k=b.location,f=k.hash;d=function(a,b){return(m=a.match(RegExp(b+"=([^&]*)")))?m[1]:null};f&&d(f,"state")&&(j=JSON.parse(decodeURIComponent(d(f,"state"))),"mpeditor"===j.action&&(b.sessionStorage.setItem("_mpcehash",f),history.replaceState(j.desiredHash||"",c.title,k.pathname+k.search)))}catch(n){}var l,h;window.mixpanel=a;a._i=[];a.init=function(b,d,g){function c(b,i){var a=i.split(".");2==a.length&&(b=b[a[0]],i=a[1]);b[i]=function(){b.push([i].concat(Array.prototype.slice.call(arguments,
0)))}}var e=a;"undefined"!==typeof g?e=a[g]=[]:g="mixpanel";e.people=e.people||[];e.toString=function(b){var a="mixpanel";"mixpanel"!==g&&(a+="."+g);b||(a+=" (stub)");return a};e.people.toString=function(){return e.toString(1)+".people (stub)"};l="disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");
for(h=0;h<l.length;h++)c(e,l[h]);var f="set set_once union unset remove delete".split(" ");e.get_group=function(){function a(c){b[c]=function(){call2_args=arguments;call2=[c].concat(Array.prototype.slice.call(call2_args,0));e.push([d,call2])}}for(var b={},d=["get_group"].concat(Array.prototype.slice.call(arguments,0)),c=0;c<f.length;c++)a(f[c]);return b};a._i.push([b,d,g])};a.__SV=1.2;b=c.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?
MIXPANEL_CUSTOM_LIB_URL:"file:"===c.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";d=c.getElementsByTagName("script")[0];d.parentNode.insertBefore(b,d)}})(document,window.mixpanel||[]);
mixpanel.init("YOUR TOKEN");</script>
  <!-- end Mixpanel -->

<script type="text/javascript">
mixpanel.track('Viewed Order Status');

mixpanel.identify("{{customer.email}}");
mixpanel.people.set_once({ 
    "$name": "{{customer.first_name}} " + "{{customer.last_name}}", 
    "$email": "{{customer.email}}" 
  });

mixpanel.people.track_charge({{checkout.total_price}} / 100); 
</script>

Step 9 – Track cart adds

Tracking cart adds are a little different than page views since a lot of stores have incorporated functionality that keeps users on the page they are viewing when they add something to cart. Regardless of how your store handles cart adds, they should all involve the same form submission so the snippet below will do the trick. Append it to the Mixpanel script tag that you have been developing in layout/theme.liquid.

$('form[action="/cart/add"] [type="submit"]').click(function(){
  mixpanel.track('Cart Add');
});

Step 10 – Logout

In this final step, we handle logouts which is important for tracking users on shared computers. You don’t want to accidentally attribute events from a different user who just happen to use the same computer as another. Add the following to the Mixpanel script tag in layout/theme.liquid.

  // Reset tracking on logout
  $('a[href*="/account/logout"]').click(function(){
    mixpanel.reset();
  });

Closing thoughts

After completing the above 10 steps, the script tag in layout/theme.liquid that contains all the Mixpanel integration logic should look like the following. It will give you the basics which is a lot in terms of analytic insights that you will now be able to get out of Mixpanel.

<script type="text/javascript">
if ('undefined' !== typeof mixpanel) {
  // Capture all page views
  mixpanel.track('Viewed Page');

  // If the user is logged in
  {% if customer %}
  // User just signed up
  if ("{{customer.email}}" === Cookies.get('registered_email')) {
    mixpanel.alias("{{customer.email}}");
    Cookies.remove('registered_email');
  } else {
    mixpanel.identify("{{customer.email}}");
  }

  mixpanel.people.set_once({ 
    "$name": "{{customer.first_name}} " + "{{customer.last_name}}", 
    "$email": "{{customer.email}}" 
  });
  {% endif %}

  // Capture cart adds
  $('form[action="/cart/add"] [type="submit"]').click(function(){
    mixpanel.track('Cart Add');
  });

  // Reset tracking on logout
  $('a[href*="/account/logout"]').click(function(){
    mixpanel.reset();
  });
}
</script>

I cannot emphasize just how important it is to understand how your users progress through your various conversion funnels and what their retention behavior looks like. Without these kinds of insights, you are basically making guesses as to how to optimize your UX. Understanding how and why your users shop your store is critical to being successful.

You should get a lot of mileage out of this tutorial. There is, however, some room for optimizations and customizations:

  • All page views are treated the same: In Mixpanel, you can leverage filters to isolate different pages in your funnel analytics however, it is a good idea to create custom events for pages that you feel are important in one or more of your funnels. That improves the depth of analytics that you can get out of Mixpanel since those custom events will then be distinct from the other page views.
  • mixpanel.identify() only needs to be called after user login: It doesn’t hurt anything by call it with every page load but it isn’t necessary either.
  • Add custom properties to events, people, and charges: Mixpanel lets you decorate your events, users, and charges with any custom property you see fit. That enables you to do some very useful event filtering and user cohort analyses.
  • Cart add and logout click events assume no DOM manipulation after page load: Typically this is a safe assumption however there are Shopify apps out there that inject in cart add forms. You will need to do something a little special to handle those apps if you have them installed in your store.

Nocking Point Coffee Club January Shipment

Well, it was a little delayed due to snowy conditions but I got my January Nocking Point Coffee Club shipment. I tried the city roast this morning and it was fantastic. Two shipments into the coffee club and I am super happy with what I am getting.

This month’s lifestyle item is a lunch bag that also doubles as a two bottle wine carrier. Remarkably, I haven’t had a lunch bag in years even though I pack healthy fruit snacks for work everyday as per my Noom lifestyle. I’ve been rocking it with a small reusable lulu lemon bag which has served me well but this Nocking Point one is just plain cool. Also, the closest thing I have had to a wine carrier are… Trader Joe bags… so that is an upgrade there too.