diff --git a/examples/pixel/README.md b/examples/pixel/README.md index b085292..607743a 100644 --- a/examples/pixel/README.md +++ b/examples/pixel/README.md @@ -1,8 +1,8 @@ # Bloomreach - Pixel instrumentation example -This example demonstrates how to instrument pixel across a simple storefront. +This example demonstrates how to instrument the pixel across a simple storefront for page view, widget view, suggest, search, click, add to cart and conversion events. -- See `src/config.js` file to configure it to run for your account and catalog +- See `src/config.js` file to configure it to run for your account and catalog. This also has the configuration to turn on debug mode for the pixel - See `src/hooks/useAnalytics.js` file to see how the pixel events are formatted - Search for the term `trackEvent` to see how the pixel events are instrumented across the sourcecode - Note: On codesandbox, because of how the preview is rendered in the iframe, the pixel script is not able to set the `_br_uid_2` cookie on the right domain, so you will not see the cookie sent in the pixel events. For the cookie to be set and sent in the pixel events, open the codesandbox preview in a new tab or run the example locally diff --git a/examples/pixel/src/app/DeveloperToolbar.jsx b/examples/pixel/src/app/DeveloperToolbar.jsx index 0e91da9..c71982e 100644 --- a/examples/pixel/src/app/DeveloperToolbar.jsx +++ b/examples/pixel/src/app/DeveloperToolbar.jsx @@ -68,7 +68,7 @@ export function DeveloperToolbar() { - {eventsCount} + {eventsCount > 25 ? '25+' : eventsCount} diff --git a/examples/pixel/src/app/cart/page.jsx b/examples/pixel/src/app/cart/page.jsx index 43f8e37..403e99f 100644 --- a/examples/pixel/src/app/cart/page.jsx +++ b/examples/pixel/src/app/cart/page.jsx @@ -6,16 +6,14 @@ import { Button, MinusIcon, PlusIcon, TrashIcon } from '@bloomreach/react-banana import { useIntersectionObserver } from 'usehooks-ts'; import { useEffect, useState } from 'react'; import useCart from '../../hooks/useCart'; -import useRecommendationsApi from '../../hooks/useRecommendationsApi'; import { similar_products_widget_id } from '../../config'; -import { ProductsCarouselWidget } from '../../components/ProductsCarouselWidget'; -import { CONFIG } from '../../constants'; + +import { ItemBasedRecommendationsWidget } from '../../components/ItemBasedRecommendationsWidget'; export default function Page() { const router = useRouter(); const { cart, incrementItem, decrementItem, removeItem, cartCount, cartTotal } = useCart(); - const [recOptions, setRecOptions] = useState({}); - const { data: similarProducts } = useRecommendationsApi(similar_products_widget_id, CONFIG, recOptions); + const [recPids, setRecPids] = useState([]); const [ref, isIntersecting] = useIntersectionObserver({ threshold: 0, root: null, @@ -23,12 +21,7 @@ export default function Page() { }); useEffect(() => { - setRecOptions({ - item_ids: cart.map((item) => item.id).join(','), - filter: `-pid:(${cart.map((item) => `"${item.id}"`).join(' OR ')})`, - rows: 4, - start: 0, - }); + setRecPids(cart.map((item) => item.id)); }, [cart]); return ( @@ -116,8 +109,13 @@ export default function Page() {
- {isIntersecting && similarProducts && ( - )} + {isIntersecting && ( + + )}
) diff --git a/examples/pixel/src/app/page.jsx b/examples/pixel/src/app/page.jsx index 2a13cf8..38277cc 100644 --- a/examples/pixel/src/app/page.jsx +++ b/examples/pixel/src/app/page.jsx @@ -3,9 +3,12 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import JsonView from '@uiw/react-json-view'; +import { useIntersectionObserver } from 'usehooks-ts'; import { ProductCard } from '../components/ProductCard'; import { useDeveloperTools } from '../hooks/useDeveloperTools'; import useSearchApi from '../hooks/useSearchApi'; +import { PersonalizedWidget } from '../components/PersonalizedWidget'; +import { recently_viewed_widget_id } from '../config'; import { CONFIG } from '../constants'; function getRandomCategories(data) { @@ -26,6 +29,11 @@ export default function Home() { q: '*', rows: 12, }); + const [ref, isIntersecting] = useIntersectionObserver({ + threshold: 0, + root: null, + rootMargin: '0px', + }); const { loading, error, data } = useSearchApi('bestseller', CONFIG, options); useEffect(() => { @@ -38,10 +46,10 @@ export default function Home() {
{loading ?
Loading...
: ( <> -
Shop by category
+
Shop by category
{showJson ? ( - + ) : (
{categories.map((category) => ( @@ -69,7 +77,7 @@ export default function Home() {
)}
-
Shop our best sellers
+
Shop our best sellers
{error && (

Error:

@@ -95,6 +103,10 @@ export default function Home() {
)}
+ +
+ {isIntersecting && ()} +
)} diff --git a/examples/pixel/src/app/products/[id]/page.jsx b/examples/pixel/src/app/products/[id]/page.jsx index 1eca8ae..50553be 100644 --- a/examples/pixel/src/app/products/[id]/page.jsx +++ b/examples/pixel/src/app/products/[id]/page.jsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import JsonView from '@uiw/react-json-view'; import { Button } from '@bloomreach/react-banana-ui'; import { useIntersectionObserver } from 'usehooks-ts'; @@ -8,9 +8,8 @@ import { Price } from '../../../components/Price'; import useCart from '../../../hooks/useCart'; import useAnalytics from '../../../hooks/useAnalytics'; import { useDeveloperTools } from '../../../hooks/useDeveloperTools'; -import { ProductsCarouselWidget } from '../../../components/ProductsCarouselWidget'; +import { ItemBasedRecommendationsWidget } from '../../../components/ItemBasedRecommendationsWidget'; import useSearchApi from '../../../hooks/useSearchApi'; -import useRecommendationsApi from '../../../hooks/useRecommendationsApi'; import { similar_products_widget_id } from '../../../config'; import { CONFIG } from '../../../constants'; @@ -20,9 +19,8 @@ export default function Page({ params }) { const { addItem } = useCart(); const { trackEvent } = useAnalytics(); const [options, setOptions] = useState({}); - const [recOptions, setRecOptions] = useState({}); + const [recPids, setRecPids] = useState([]); const { loading, error, data } = useSearchApi('keyword', CONFIG, options); - const { data: similarProducts } = useRecommendationsApi(similar_products_widget_id, CONFIG, recOptions); const [ref, isIntersecting] = useIntersectionObserver({ threshold: 0, root: null, @@ -36,29 +34,26 @@ export default function Page({ params }) { rows: 1, fq: `pid:"${pid}"`, }); - setRecOptions({ - item_ids: pid, - filter: `-pid:("${pid}")`, - rows: 4, - start: 0, - }); + + setRecPids([pid]); } }, [pid]); - const product = data?.response?.docs[0]; - const sku = product?.variants.length ? product.variants[0].skuid : undefined; + const product = useMemo(() => data?.response?.docs[0], [data]); + const sku = useMemo(() => product && product.variants.length ? product.variants[0].skuid : undefined, [product]); + const title = useMemo(() => product ? product.title : undefined, [product]); useEffect(() => { - if (!product) { + if (!pid || !title) { return; } trackEvent({ event: 'view_product', - pid: product.pid, - title: product.title, + pid, + title, sku, }); - }, [product, sku]); + }, [pid, sku]); const addToCart = (item) => { addItem({ @@ -122,7 +117,7 @@ export default function Page({ params }) { )}
- {isIntersecting && similarProducts && ()} + {isIntersecting && ()}
); diff --git a/examples/pixel/src/components/ItemBasedRecommendationsWidget.jsx b/examples/pixel/src/components/ItemBasedRecommendationsWidget.jsx new file mode 100644 index 0000000..e7a9d1a --- /dev/null +++ b/examples/pixel/src/components/ItemBasedRecommendationsWidget.jsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; +import useAnalytics from '../hooks/useAnalytics'; +import useItemBasedRecommendationsApi from '../hooks/useItemBasedRecommendationsApi'; +import { ProductsCarouselWidget } from './ProductsCarouselWidget'; +import { CONFIG } from '../constants'; + +export function ItemBasedRecommendationsWidget({ title = 'Similar products', widgetId, pids }) { + const { trackEvent } = useAnalytics(); + const [options, setOptions] = useState({}); + const { data } = useItemBasedRecommendationsApi(widgetId, CONFIG, options); + + useEffect(() => { + if (pids && pids.length) { + setOptions({ + item_ids: pids.join(','), + filter: `-pid:(${pids.map((pid) => `"${pid}"`).join(' OR ')})`, + rows: 4, + start: 0, + }); + } + }, [pids]); + + useEffect(() => { + if (!data?.metadata) { + return; + } + + trackEvent({ + event: 'event_widgetView', + widgetId: data.metadata.widget.id, + widgetType: data.metadata.widget.type, + widgetRequestId: data.metadata.widget.rid, + }); + }, [data]); + + + if (!data?.response) { + return null; + } + + return ( +
+
+
{title}
+
+ {data && ()} +
+ ); +} diff --git a/examples/pixel/src/components/PersonalizedWidget.jsx b/examples/pixel/src/components/PersonalizedWidget.jsx new file mode 100644 index 0000000..173a7ce --- /dev/null +++ b/examples/pixel/src/components/PersonalizedWidget.jsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import useAnalytics from '../hooks/useAnalytics'; +import usePersonalizedWidgetApi from '../hooks/usePersonalizedWidgetApi'; +import { ProductsCarouselWidget } from './ProductsCarouselWidget'; +import { CONFIG } from '../constants'; + +export function PersonalizedWidget({ widgetId, title = 'Recently viewed products' }) { + const { trackEvent } = useAnalytics(); + const [options] = useState({ rows: 4, start: 0 }); + const { data } = usePersonalizedWidgetApi(widgetId, CONFIG, options); + + useEffect(() => { + if (!data?.metadata) { + return; + } + + trackEvent({ + event: 'event_widgetView', + widgetId: data.metadata.widget.id, + widgetType: data.metadata.widget.type, + widgetRequestId: data.metadata.widget.rid, + }); + }, [data]); + + + if (!data?.response) { + return null; + } + + return ( +
+
+
{title}
+
+ {data && ()} +
+ ); +} diff --git a/examples/pixel/src/components/ProductsCarouselWidget.jsx b/examples/pixel/src/components/ProductsCarouselWidget.jsx index bb8d3bf..4f31e6a 100644 --- a/examples/pixel/src/components/ProductsCarouselWidget.jsx +++ b/examples/pixel/src/components/ProductsCarouselWidget.jsx @@ -1,11 +1,9 @@ -import { useEffect } from 'react'; -import { InfoIcon, Tooltip } from '@bloomreach/react-banana-ui'; import JsonView from '@uiw/react-json-view'; import { ProductCard } from './ProductCard'; import useAnalytics from '../hooks/useAnalytics'; import { useDeveloperTools } from '../hooks/useDeveloperTools'; -export function ProductsCarouselWidget({ title = 'Similar products', data }) { +export function ProductsCarouselWidget({ data }) { const { showJson } = useDeveloperTools(); const { trackEvent } = useAnalytics(); @@ -19,33 +17,12 @@ export function ProductsCarouselWidget({ title = 'Similar products', data }) { }); } - useEffect(() => { - if (!data?.metadata) { - return; - } - - trackEvent({ - event: 'event_widgetView', - widgetId: data.metadata.widget.id, - widgetType: data.metadata.widget.type, - widgetRequestId: data.metadata.widget.rid, - }); - }, [data]); - if (!data?.response) { return null; } return (
-
-
{title}
-
- - - -
-
{showJson ? ( ) : ( diff --git a/examples/pixel/src/components/SearchBar.jsx b/examples/pixel/src/components/SearchBar.jsx index 041a534..59a20e3 100644 --- a/examples/pixel/src/components/SearchBar.jsx +++ b/examples/pixel/src/components/SearchBar.jsx @@ -228,7 +228,8 @@ function SearchBarComponent() { {error ? : null} {query && data ? (
-
@@ -245,7 +250,7 @@ function SearchBarComponent() { {query}
-
+
diff --git a/examples/pixel/src/config.js b/examples/pixel/src/config.js index d3fb796..e179ed5 100644 --- a/examples/pixel/src/config.js +++ b/examples/pixel/src/config.js @@ -4,12 +4,13 @@ export const domain_key = 'showcase_pacifichome'; export const account_name = 'showcase-pacifichome.bloomreach.io'; export const catalog_views = 'showcase_pacifichome'; -// Sets the pixel to debug mode, so the events will not be used as learning data for Bloomreach's -// algorithms and you can see the pixels in realtime on Event Management -export const debugPixel = true; +// Setting this to true sets the pixel to debug mode, so the events will not be used as learning data for Bloomreach's +// algorithms and you can see the pixels in real time on Event Management +export const debugPixel = false; -// Comment the line below, if the similar products widget does not exist in your account +// Comment the lines below, if any of the widgets do not exist in your account export const similar_products_widget_id = '09zwz059'; +export const recently_viewed_widget_id = '2l7eo489'; export const product_fields = [ 'pid', 'score', 'is_live', 'title', 'description', 'brand', diff --git a/examples/pixel/src/hooks/useAnalytics.js b/examples/pixel/src/hooks/useAnalytics.js index 5c185a1..07d9554 100644 --- a/examples/pixel/src/hooks/useAnalytics.js +++ b/examples/pixel/src/hooks/useAnalytics.js @@ -155,7 +155,7 @@ function useAnalytics() { break; } - setEvents(_.take([...[{ ...payload, ...{ event: data.event } }], ...events], 25)); + setEvents(_.take([...[{ ...payload, ...{ event: data.event } }], ...events], 26)); }, [constructPayload, events, setEvents]); function clearEvents() { diff --git a/examples/pixel/src/hooks/useItemBasedRecommendationsApi.js b/examples/pixel/src/hooks/useItemBasedRecommendationsApi.js new file mode 100644 index 0000000..e1f9eaf --- /dev/null +++ b/examples/pixel/src/hooks/useItemBasedRecommendationsApi.js @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import { getItemWidget } from '@bloomreach/discovery-web-sdk'; +import { useCookies } from 'react-cookie'; +import { BR_COOKIE } from '../constants'; +import { product_fields } from '../config'; + +function useItemBasedRecommendationsApi(widgetId, config, options) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [cookies] = useCookies([BR_COOKIE]); + + useEffect(() => { + if (!options.item_ids) { + setLoading(false); + setError(null); + setData(null); + return; + } + + setLoading(true); + getItemWidget( + widgetId, + config, + { + ...{ + _br_uid_2: cookies[BR_COOKIE], + fields: product_fields, + url: window.location.href, + ref_url: window.location.href, + request_id: Date.now(), + br_diagnostic: 'all', + }, + ...options, + }, + ).then((res) => { + setLoading(false); + setError(null); + setData(res); + }, (err) => { + setLoading(false); + setError(err); + setData(null); + }); + }, [widgetId, config, options, cookies]); + + return { data, loading, error }; +} +export default useItemBasedRecommendationsApi; diff --git a/examples/pixel/src/hooks/useRecommendationsApi.js b/examples/pixel/src/hooks/usePersonalizedWidgetApi.js similarity index 86% rename from examples/pixel/src/hooks/useRecommendationsApi.js rename to examples/pixel/src/hooks/usePersonalizedWidgetApi.js index 7004db6..1c22d46 100644 --- a/examples/pixel/src/hooks/useRecommendationsApi.js +++ b/examples/pixel/src/hooks/usePersonalizedWidgetApi.js @@ -4,17 +4,13 @@ import { useCookies } from 'react-cookie'; import { BR_COOKIE } from '../constants'; import { product_fields } from '../config'; -function useRecommendationsApi(widgetId, config, options) { +function usePersonalizedWidgetApi(widgetId, config, options) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [cookies] = useCookies([BR_COOKIE]); useEffect(() => { - if (!widgetId || !options.item_ids) { - return; - } - setLoading(true); getItemWidget( widgetId, @@ -43,4 +39,4 @@ function useRecommendationsApi(widgetId, config, options) { return { data, loading, error }; } -export default useRecommendationsApi; +export default usePersonalizedWidgetApi;