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.

Dirtbag by Jason Momoa

I have made my first wine purchase from Nocking Point, about a month after I joined their coffee club. I’ve only gotten one coffee shipment from them but I’ve been happy so far. Now this wine clearly is for something else.

My first half marathon of the year is coming up in March so I decided as a reward, I would get this red wine blend by none other than Jason Momoa, a.k.a. Aquaman, a.k.a. Khal Drogo, a.k.a. Conan, a.k.a. cool dad. I really love him as an actor and every time I read something about him I’m pleasantly surprised. It was a no brainer for me to pull the trigger on the red wine he consulted on at Nocking Point as my post race drink. It is suppose to be big, bold, and spicy which is what I like. Hopefully I will be pleasantly surprised.

Noom friendly microwave cake

I have to say that this microwave cake is by far my favorite of my recent cooking discoveries. It is simple and fast to cook. It is easy to involve the kids at every stage from mashing up the bananas to stirring the bowl to pushing buttons on the microwave. It is super tasty and takes no time to decorate and garnish with fruit. Both my kids love it as a weekend breakfast treat and it’s only 380 calories!

New 1 mile personal best – 5:48

Today Orangetheory had a 1 mile benchmark and I was able to hit a brand new personal best, 5 minutes and 48 seconds. While I know it is on a treadmill and running outside is slower, I am still really happy about it. When I joined Orangetheory two years ago, I clocked in at 6:27 for a mile. I have been steadily improving yet I never was able to break the 6 minute mark. I crushed it today and it is definitely related to all the weight I’ve lost on Noom.

Noom friendly easy strawberry rhubarb pie filling

Over the holidays I was introduced to the miracle that is the no bake pie. Honestly, I knew about them but I never really gave them the time of day. I grew up where pies were something that you put in the oven, applied lots of heat, and needed an oven mitt to get back out. With young kids that you are trying to involve in cooking, that isn’t going to work so I decided to give no bake pies a hard look and I was pleasantly surprised.

I managed to come up with this recipe using sugar free jello and pudding mixes. I used a microwave to heat up frozen berries while stirring in the jello mix with the kids. Then we mixed in pudding and that gave us a super tasty pie filling that is also low calorie: a remarkable 38 calories per serving! My recipe makes 16 servings, 2 pies worth. You pair this with the right pie crust and you have a totally guilt free pie. In fact, you can also just pour it into bowls if you want to skip the crust.

os.detected maven properties not detected by eclipse

I recently hit this weird error with one of my Java projects using maven. It would build perfectly fine from command line but fail in eclipse. It was due to eclipse not being able to recognize this property, os.detected.classifier, that we had added to one of our pom files. This property is handled by the os maven plugin and is pretty useful for managing dependencies with os specific artifacts. Using this property enables us to use the same pom on different os’s which is important when you want to support people using Windows, OSX, and linux. The plugin works properly from command line but trips up eclipse.

I googled around for how different folks have solved this problem and in my opinion, the simplest solution is to update your ~/.m2/settings.xml to hard code values for os.detected properties. For example, I use a mac so my settings.xml looks like the following:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                          https://maven.apache.org/xsd/settings-1.0.0.xsd">

<profiles>
  <profile>
    <id>os-properties</id>
    <properties>
      <os.detected.name>osx</os.detected.name>
      <os.detected.arch>x86_64</os.detected.arch>
      <os.detected.classifier>osx-x86_64</os.detected.classifier>
    </properties>
  </profile>
</profiles>

<activeProfiles>
  <activeProfile>os-properties</activeProfile>
</activeProfiles>

</settings>

Whole Wheat M&M Oatmeal Cookies

Noom really did change my life. One of those changes was a complete lack of healthy treats that I would consider to be a reward. This oatmeal cookie was the first thing I discovered that has enough sweetness to make me happy while at the same time is filling enough that I only need to have one at the end of the day.

I tend to make these cookies a little thicker than the flat discs that most people like to bake. I do that partial to keep the bottom from sticking to the pan and partially to keep the M&Ms balled up inside.

Orangetheory Base Pace Update: 8.2

It feels a little foolish to write this but I didn’t realize just how much speed I had locked up in my weight. Orangetheory is a great workout for me and I’ve been going for the last two years. Since I joined, my base pace on the treadmill has slowly improved from 6.0 to 7.2. I recently dropped over 35 lbs using Noom and my base pace comfortably jumped up to 8.2.

When you think about what a 35 lbs dumbbell feels like, it shouldn’t be all that surprising that when you drop that kind of body mass that of course you will run faster. I guess it just wasn’t something that had crossed my mind. It certainly wasn’t something that I had set out to do when I started Noom but I am definitely going to take it.

It’s a little wet for my tastes to do a 10k outside; yes, I am made of sugar. I am pretty itchy for a race though with my new found speed. The next one I am registered for is the Mercer Island Half. I’m going to start doing some indoor ramping this week but still, I’m really curious about how it is going to go for me outside.