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) => {
FastSimonReporting.prepareProductSeenFromCollectionData({
productId,
productPosition: collection.items.findIndex(item => item.id === productId) + 1,
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>
);
}