Yotpo SMS & Email Web Pixel Installation Guide for Shopify Hydrogen 2
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, these utility hooks for working with analytics are not shipped out of the box when you create a Hydrogen project using npm create @shopify/hydrogen
.
Make sure the following 2 hooks are present in app/utils.js
:
import {useFetchers, useMatches} from '@remix-run/react';
export function useAnalyticsFromLoaders(dataKey= 'analytics') {
const matches = useMatches();
const data= {};
matches.forEach((event) => {
const eventData = event?.data;
if (eventData && eventData[dataKey]) {
Object.assign(data, eventData[dataKey]);
}
});
return data;
}
export function useAnalyticsFromActions(dataKey = 'analytics') {
const fetchers = useFetchers();
const data = {};
for (const fetcher of fetchers) {
const formData = fetcher.formData || fetcher.submission?.formData;
const fetcherData = fetcher.data;
// Make sure that you have a successful action and an analytics payload
if (formData && fetcherData && fetcherData[dataKey]) {
Object.assign(data, fetcherData[dataKey]);
try {
if (formData.get(dataKey)) {
// If the form submission contains data for the same dataKey
// and is JSON parseable, then combine it with the resulting object
const dataInForm = JSON.parse(String(formData.get(dataKey)));
Object.assign(data, dataInForm);
}
} catch {
// do nothing
}
}
}
return Object.keys(data).length ? data : undefined;
}
Although we provide here the useAnalyticsFromLoaders()
and useAnalyticsFromActions()
hooks for thoroughness, it's best practice to check the original guide for updated versions.
useYotpoSms()
hook and context key constants
Copy yotposms.jsx to your project. It provides 3 exports:
- The
useYotpoSms( options )
hook, which will load the SDK script and expose the data layer object - YOTPOSMS_ANALYTICS_PAGEVIEW constant
- YOTPOSMS_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',
+ }
+ }
});
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:
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 analytics
prop to your <AddToCartButton/>
component.
<AddToCartButton
+ analytics={{
+ [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/', ''),
+ }
+ }}
>
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 { useAnalyticsFromActions, useAnalyticsFromLoaders } from './utils';
In the loader()
, after validating 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},
);
Finally, we will place catch all analytics data.
export default function App() {
const data = useLoaderData();
+ const yotpoDataLayerRef = useYotpoSms({
+ store: data.publicStoreDomain,
+ })
+ const location = useLocation();
+ const pageAnalytics = useAnalyticsFromLoaders();
+ const analyticsFromActions = useAnalyticsFromActions();
+ const analytics = useMemo(
+ () => ({ ...pageAnalytics, ...analyticsFromActions }),
+ [pageAnalytics, analyticsFromActions]
+ )
+ useEffect(() => {
+ if (analytics[YOTPOSMS_ANALYTICS_PAGEVIEW]) {
+ yotpoDataLayerRef.current.push(pageAnalytics[YOTPOSMS_ANALYTICS_PAGEVIEW])
+ }
+ }, [location.pathname]);
+ useEffect(() => {
+ if (analytics[YOTPOSMS_ANALYTICS_EVENT]) {
+ yotpoDataLayerRef.current.push(analytics[YOTPOSMS_ANALYTICS_EVENT])
+ }
+ }, [analytics[YOTPOSMS_ANALYTICS_EVENT]])
return (
<html lang="en">
...
</html>
)
}