- DarkLight
Yotpo SMS & Email Web Pixel Installation Guide for Shopify Hydrogen 2
- DarkLight
Use this repository as a guideline for implementing Yotpo SMS customer behavior tracking on Shopify Hydrogen 2 storefronts that are using the Remix.run framework.
Before you proceed, please make sure you have the Yotpo SMS & Email (SMSBump) sales channel installed on your Shopify store.
Dependencies
Make sure you have the @shopify/hydrogen-react
library added to your dependencies. We will use it to load the JS SDK script.
Analytics layer in Hydrogen 2
The examples shown here are based on the recommended approach of collecting analytics, as described in this Shopify article.
At the time of writing this guide, the usePageAnalytics()
hook is not shipped out of the box when you create a hydrogen project using npm create @shopify/hydrogen
.
Make sure you add it to app/utils.js
:
import { useMatches, useMemo } from '@remix-run/react'
export function usePageAnalytics() {
const matches = useMatches();
const analyticsFromMatches = useMemo(() => {
const data = {};
matches.forEach((event) => {
const eventData = event?.data;
if (eventData) {
eventData['analytics'] && Object.assign(data, eventData['analytics']);
}
});
return data;
}, [matches]);
return analyticsFromMatches;
}
Although the usePageAnalytics()
hook is provided for thoroughness, it's generally a good idea to check the original guide for an updated version.
useYotpoSms()
hook and context key constants
Create an app/analytics/yotposms.js
.
import { useLoadScript } from "@shopify/hydrogen-react";
import { useEffect, useRef } from "react";
export const YOTPOSMS_ANALYTICS_PAGEVIEW = 'yotpoSmsPageview'
export const YOTPOSMS_ANALYTICS_EVENT = 'yotpoSmsEvent'
export default function useYotpoSms(opts = {}) {
const dataLayer = useRef([])
Object.assign(opts, {
platform: 2,
})
useLoadScript('https://d18eg7dreypte5.cloudfront.net/browse-abandonment/v2/generic.js')
useEffect(() => {
window.wtba = window.wtba || []
if (Array.isArray(dataLayer.current) && dataLayer.current.length) {
window.wtba.push(...dataLayer.current)
}
dataLayer.current = window.wtba
dataLayer.current.push({ type: 'config', options: opts })
}, [])
return dataLayer
}
It provides 3 exports:
- the
useYotpoSms( options )
hook, which will load the SDK script and expose the data layer object YOTPOSMS_ANALYTICS_PAGEVIEW
constantYOTPOSMS_ANALYTICS_EVENT
constant
Update routes and actions
You need to update your routes and actions so they can start broadcasting analytics data.
Homepage route
import { YOTPOSMS_ANALYTICS_PAGEVIEW } from '~/analytics/yotposms';
Update your json()
/defer()
within the loader()
of app/routes/_index.jsx
, so that it looks as follows:
return defer({
featuredCollection,
recommendedProducts,
+ analytics: {
+ [YOTPOSMS_ANALYTICS_PAGEVIEW]: {
+ type: 'home',
+ }
+ }
});
Update AddToCartButton component
If you followed Shopify’s guide on tracking add-to-cart events, then you should have the AddToCartAnalytics
Higher Order Component. We want to use the useYotpoSms()
hook there so we can push those events to the data layer.
In your app/components/AddToCartButton.jsx
:
import { useRouteLoaderData } from "@remix-run/react";
import useYotpoSms, { YOTPOSMS_ANALYTICS_EVENT } from "~/analytics/yotposms";
function AddToCartAnalytics({
fetcher,
children,
}) {
// Data from action response
const fetcherData = fetcher.data;
// Data in form inputs
const formData = fetcher.formData;
+ const rootData = useRouteLoaderData('root');
+
+ const yotpoDataLayerRef = useYotpoSms({
+ store: rootData.publicStoreDomain,
+ })
+
useEffect(() => {
if (formData) {
const cartData = {};
const cartInputs = CartForm.getFormInput(formData);
try {
// Get analytics data from form inputs
if (cartInputs.inputs.analytics) {
const dataInForm = JSON.parse(
String(cartInputs.inputs.analytics),
);
Object.assign(cartData, dataInForm);
}
} catch {
// do nothing
}
// If we got a response from the add to cart action
if (Object.keys(cartData).length && fetcherData) {
// send add to cart event
+ cartData.products?.forEach(event => {
+ if (event && event[YOTPOSMS_ANALYTICS_EVENT]) {
+ yotpoDataLayerRef.current.push(event[YOTPOSMS_ANALYTICS_EVENT])
+ }
+ })
}
}
}, [fetcherData, formData]);
return <>{children}</>;
}
Product route
import { YOTPOSMS_ANALYTICS_EVENT, YOTPOSMS_ANALYTICS_PAGEVIEW } from '~/analytics/yotposms';
Update your json()
/defer()
within loader()
of app/routes/products.$handle.jsx
, so that it looks as follows:
return defer({
product,
variants,
+ analytics: {
+ [YOTPOSMS_ANALYTICS_PAGEVIEW]: {
+ type: 'product',
+ id: product.id.replace('gid://shopify/Product/', ''),
+ variant_id: product.selectedVariant.id.replace('gid://shopify/ProductVariant/', ''),
+ }
+ }
});
In order to receive analytics on products added to cart, add the productAnalytics
prop to your <AddToCartButton/>
component.
<AddToCartButton
// existing props,
+ productAnalytics={{
+ [YOTPOSMS_ANALYTICS_EVENT]: {
+ type: 'event',
+ event: 'product_added_to_cart',
+ id: product.id.replace('gid://shopify/Product/', ''),
+ variant_id: selectedVariant.id.replace('gid://shopify/ProductVariant/', ''),
+ }
+ }}
>
{selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
</AddToCartButton>
Collection route
import { YOTPOSMS_ANALYTICS_PAGEVIEW } from '~/analytics/yotposms';
Update yourjson()/defer() within loader() oflapp/routes/collections.$handle.jsx, so it looks as follows:
return json({
collection,
+ analytics: {
+ [YOTPOSMS_ANALYTICS_PAGEVIEW]: {
+ type: 'collection',
+ id: collection.id.replace('gid://shopify/Collection/', ''),
+ }
+ }
});
Logout route
We will set a flash session on customer logout to be able to notify the tracking script about this event.
Update the action()
handler of your app/routes/account_.logout.jsx
, setting the session just before the redirect()
.
+ session.flash('customerJustLoggedOut', true)
return redirect('/', {
headers: {
'Set-Cookie': await session.commit(),
},
});
Account route
We will use the account route to "catch" the current logged-in customer. This is purely illustrative and we do it this way here because customers are redirected to the profile page upon login, and we already have the customer object there. You can achieve the same on any other route if your use case differs.
import { YOTPOSMS_ANALYTICS_EVENT } from '~/analytics/yotposms';
Update your loader()
within app/routes/account.jsx
route, so that it looks as follows:
const { customer } = await storefront.query(CUSTOMER_QUERY, {
variables: {
customerAccessToken: customerAccessToken.accessToken,
country: storefront.i18n.country,
language: storefront.i18n.language,
},
cache: storefront.CacheNone(),
});
if (!customer) {
throw new Error('Customer not found');
}
+ const analytics = {}
+
+ Object.assign(analytics, {
+ [YOTPOSMS_ANALYTICS_EVENT]: (({ email, phone }) => ({
+ type: 'customer',
+ event: 'customer_login',
+ customer: { phone, email },
+ }))(customer)
+ })
return json(
- { isLoggedIn, isPrivateRoute, isAccountHome, customer },
+ { isLoggedIn, isPrivateRoute, isAccountHome, customer, analytics },
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
},
);
App Root (app/root.jsx
)
import { useLocation } from '@remix-run/react';
import { useEffect, useMemo } from 'react';
import useYotpoSms, { YOTPOSMS_ANALYTICS_EVENT, YOTPOSMS_ANALYTICS_PAGEVIEW } from './analytics/yotposms';
import { usePageAnalytics } from './utils';
In the loader()
, after validating the customer access token, check for the flash logout session and pass it down to <App/>
using json()
/defer()
.
// validate the customer access token is valid
const {isLoggedIn, headers} = await validateCustomerAccessToken(
customerAccessToken,
session,
);
...
+ const analytics = {}
+
+ if (! isLoggedIn) {
+ if (session.get('customerJustLoggedOut')) {
+ Object.assign(analytics, {
+ [YOTPOSMS_ANALYTICS_EVENT]: {
+ type: 'customer',
+ event: 'customer_logout',
+ }
+ })
+
+ headers.append('Set-Cookie', await session.commit())
+ }
+ }
return defer(
{
cart: cartPromise,
footer: footerPromise,
header: await headerPromise,
isLoggedIn,
publicStoreDomain,
+ analytics,
},
{headers},
);
Lastly, update your<App /> component:
export default function App() {
const data = useLoaderData();
+ const yotpoDataLayerRef = useYotpoSms({
+ store: data.publicStoreDomain,
+ })
+
const location = useLocation();
const lastLocationKey = useRef('');
const pageAnalytics = usePageAnalytics();
useEffect(() => {
// Filter out useEffect running twice
if (lastLocationKey.current === location.key) return;
lastLocationKey.current = location.key;
+
+ if (pageAnalytics[YOTPOSMS_ANALYTICS_PAGEVIEW]) {
+ yotpoDataLayerRef.current.push(pageAnalytics[YOTPOSMS_ANALYTICS_PAGEVIEW])
+ }
+
+ if (pageAnalytics[YOTPOSMS_ANALYTICS_EVENT]) {
+ yotpoDataLayerRef.current.push(pageAnalytics[YOTPOSMS_ANALYTICS_EVENT])
+ }
}, [location, pageAnalytics]);
return (
<html lang="en">
...
</html>
)
}