Skip to main content

Analytics Usage

This guide explains how to integrate Fast Simon analytics with your Shopify Hydrogen store using the provided code examples.

Overview​

This implementation demonstrates how to track and report analytics data, including product views and collection views, through Fast Simon's storefront kit in a Shopify Hydrogen setup.

Usage Example​

Implementation on file \app\root.tsx​

import { FastSimonAnalytics } from '@fast-simon/storefront-kit';
... // additional imports
export async function loader(args: LoaderFunctionArgs) {
// Start fetching non-critical data without blocking time to first byte
const deferredData = loadDeferredData(args);

// Await the critical data required to render initial state of the page
const criticalData = await loadCriticalData(args);

const {storefront, env} = args.context;

return defer({
...deferredData,
...criticalData,
publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
shop: getShopAnalytics({
storefront,
publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
}),
consent: {
checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
withPrivacyBanner: false,
// localize the privacy banner
country: args.context.storefront.i18n.country,
language: args.context.storefront.i18n.language,
},
fastSimon: {
uuid: args.context.env.FAST_SIMON_UUID,
storeId: args.context.env.FAST_SIMON_STORE_ID,
storeDomain: args.context.env.PUBLIC_STORE_DOMAIN
},
});
}


... // more functions and logic
export function Layout({children}: {children?: React.ReactNode}) {
const nonce = useNonce();
const data = useRouteLoaderData<RootLoader>('root');

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{data ? (
<Analytics.Provider
cart={data.cart}
shop={data.shop}
consent={data.consent}
>
<PageLayout {...data}>{children}</PageLayout>
<FastSimonAnalytics storeId={data.fastSimon.storeId} uuid={data.fastSimon.uuid} storeDomain={data.fastSimon.storeDomain}/>

</Analytics.Provider>
) : (
children
)}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
);
}

... // more functions and logic

Implementation on file \app\entry.server.tsx​

import type {EntryContext, AppLoadContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
context: AppLoadContext,
) {
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
connectSrc: ['https://ping.fastsimon.com'],
imgSrc: [
"'self'",
'https://cdn.shopify.com',
'https://shopify.com',
'https://demo-shopify.fastsimon.com',
'https://assets.instantsearchplus.com',
'http://magento.instantsearchplus.com',
],
});

const body = await renderToReadableStream(
<NonceProvider>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>,
{
nonce,
signal: request.signal,
onError(error) {
// eslint-disable-next-line no-console
console.error(error);
responseStatusCode = 500;
},
},
);

if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');
responseHeaders.set('Content-Security-Policy', header);

return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}

Implementation on file \app\routes\products.$handle.tsx​

import {getSelectedProductOptions,  useOptimisticVariant,  CacheLong,    Analytics} from '@shopify/hydrogen';
... // additional imports
export default function Product() {
const {product, variants, visualSimilarityProducts} =
useLoaderData<typeof loader>();
const selectedVariant = useOptimisticVariant(
product.selectedVariant,
variants,
);
const widgetProducts = transformToShopifyStructure(visualSimilarityProducts);

const {title, descriptionHtml} = product;

return (
<div className="product">
<ProductImage image={selectedVariant?.image} />
<div className="product-main">
<h1>{title}</h1>
<ProductPrice
price={selectedVariant?.price}
compareAtPrice={selectedVariant?.compareAtPrice}
/>
<br />
<Suspense
fallback={
<ProductForm
product={product}
selectedVariant={selectedVariant}
variants={[]}
/>
}
>
<Await
errorElement="There was a problem loading product variants"
resolve={variants}
>
{(data) => (
<ProductForm
product={product}
selectedVariant={selectedVariant}
variants={data?.product?.variants.nodes || []}
/>
)}
</Await>
</Suspense>
<br />
<br />
<p>
<strong>Description</strong>
</p>
<br />
<div dangerouslySetInnerHTML={{__html: descriptionHtml}} />
<br />
</div>

<Analytics.ProductView
data={{
products: [
{
id: product.id,
title: product.title,
price: selectedVariant?.price.amount || '0',
vendor: product.vendor,
variantId: selectedVariant?.id || '',
variantTitle: selectedVariant?.title || '',
quantity: 1,
},
],
}}
/>
</div>
);
}

Implementation on file \app\routes\collections.$handle.tsx​

import {transformToShopifyStructure, PaginationBar, FastSimonReporting} from '@fast-simon/storefront-kit';
import {Narrow} from '@fast-simon/utilities';
... // additional imports
export async function loader(args: LoaderFunctionArgs) {
// Start fetching non-critical data without blocking time to first byte
const deferredData = loadDeferredData(args);

// Await the critical data required to render initial state of the page
const criticalData = await loadCriticalData(args);


const facets = {
facets: criticalData.collection.getFacetsOnly
? criticalData.collection.getFacetsOnly()
: criticalData.collection.facets,
};
const dashboardConfig = {
dashboardConfig: args.context.fastSimon.getDashboardConfig({
cacheStrategy: CacheLong(),
}),
};
return defer({...criticalData, ...facets, ...dashboardConfig});
}


async function loadCriticalData({
context,
params,
request,
}: LoaderFunctionArgs) {
const {handle} = params;
const {fastSimon} = context;

if (!handle) {
throw redirect('/collections');
}

const url = new URL(request.url);
const page = Number(url.searchParams.get('page')) || 1;
const narrowString = url.searchParams.get('filters');
const sortBy = url.searchParams.get('sort');
const narrow = narrowString
? Narrow.toServerNarrow(Narrow.parseNarrow(narrowString || ''))
: [];
const collection = await fastSimon.getSmartCollection({
props: {
categoryURL: '/collections/' + handle,
page,
narrow,
facetsRequired: true,
productsPerPage: 20,
categoryID: undefined,
sortBy,
},
});

if (!collection) {
throw new Response(`Collection ${handle} not found`, {
status: 404,
});
}
const transformed = transformToShopifyStructure(collection.items);
collection.products = transformed.products;
collection.handle = collection.category_url.split('/')[1];
collection.title = collection.category_name;
collection.description = collection.category_description;
return {
collection,
};
}


function loadDeferredData({context}: LoaderFunctionArgs) {
return {};
}
export default function Collection() {
const {collection, facets, dashboardConfig} = useLoaderData<typeof loader>();
const onProductClick = (productId) => {
const position = collection.ids.findIndex(id => id === productId) + 1;

// Product seen tracking
FastSimonReporting.prepareProductSeenFromCollectionData({
productId,
productPosition: position,
categoryId: collection.category_id,
categoryName: collection.category_name,
sortBy: collection.sort_by,
pageNumber: collection.p
});

// Promo tile clicked tracking
const promoTile = result.promoTiles?.find(tile => tile.id === productId);
if (promoTile.promotile) {
FastSimonReporting.preparePromotileClickedData({
id: promoTile.id,
image: promoTile.image,
link: promoTile.link,
title: promoTile.title,
position,
sortBy: collection.sort_by,
pageNumber: collection.p,
categoryId: collection.category_id,
categoryName: collection.category_name
});
}
}

return (
<div className="collection">
<h1>{collection.title}</h1>
<p className="collection-description">{collection.description}</p>
<div className={'results-filters-container'}>
<div className={'fs-products-summary'}>
<ProductsGrid products={collection.products.nodes} onProductClick={onProductClick}/>
</div>
</div>
<br />
<PaginationBar total={collection.total_p} />
<Analytics.CollectionView data={{collection: {handle: collection.handle, id: collection.category_id}}} customData={collection} />

</div>
);
}
function ProductsGrid({products, onProductClick}) {

return (
<div className="products-grid">
{products.map((product, index) => {
return (
<ProductItem
key={product.id}
product={product}
loading={index < 8 ? 'eager' : undefined}
onProductClick={onProductClick}
/>
);
})}
</div>
);
}
function ProductItem({
product,
loading,
onProductClick
}: {
product: ProductItemFragment;
loading?: 'eager' | 'lazy';
onProductClick: (productId: string) => void;
}) {
const variant = product.variants.nodes[0];
const variantUrl = useVariantUrl(product.handle, variant.selectedOptions).replace('?=', '');

return (
<Link
className="product-item"
key={product.id}
prefetch="intent"
to={variantUrl}
onClick={() => onProductClick(product.id)}
>
{product.featuredImage && (
<Image
alt={product.featuredImage.altText || product.title}
aspectRatio="0.714"
data={product.featuredImage}
loading={loading}
sizes="(min-width: 45em) 400px, calc(50vw - 2rem)"
/>
)}
<h4>{product.title}</h4>
<small>
<Money data={product.priceRange.minVariantPrice} />
</small>
</Link>
);
}