Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

W-13969523: Integrate Active data scripts with PWA Kit #1555

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'
import {useHistory, useLocation} from 'react-router-dom'
import {StorefrontPreview} from '@salesforce/commerce-sdk-react/components'
import {getAssetUrl} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils'
import useActiveData from '@salesforce/retail-react-app/app/hooks/use-active-data'
import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {useQuery, useQueries} from '@tanstack/react-query'
Expand Down Expand Up @@ -70,10 +71,12 @@ import {
THEME_COLOR,
CAT_MENU_DEFAULT_NAV_SSR_DEPTH,
CAT_MENU_DEFAULT_ROOT_CATEGORY,
DEFAULT_LOCALE
DEFAULT_LOCALE,
ACTIVE_DATA_ENABLED
} from '@salesforce/retail-react-app/app/constants'

import Seo from '@salesforce/retail-react-app/app/components/seo'
import {Helmet} from 'react-helmet'

const onClient = typeof window !== 'undefined'

Expand Down Expand Up @@ -115,6 +118,7 @@ const App = (props) => {
const categories = flatten(categoriesTree || {}, 'categories')
const {getTokenWhenReady} = useAccessToken()
const appOrigin = getAppOrigin()
const activeData = useActiveData()

const history = useHistory()
const location = useLocation()
Expand Down Expand Up @@ -269,9 +273,26 @@ const App = (props) => {
history.push(path)
}

const trackPage = () => {
activeData.trackPage(site.id, locale.id, currency)
}

useEffect(() => {
trackPage()
}, [location])

return (
<Box className="sf-app" {...styles.container}>
<StorefrontPreview getToken={getTokenWhenReady}>
<Helmet>
{ACTIVE_DATA_ENABLED && (
<script
src={getAssetUrl('static/head-active_data.js')}
id="headActiveData"
type="text/javascript"
></script>
)}
</Helmet>
<IntlProvider
onError={(err) => {
if (!messages) {
Expand Down Expand Up @@ -396,6 +417,23 @@ const App = (props) => {
</Box>
</CurrencyProvider>
</IntlProvider>
{ACTIVE_DATA_ENABLED && (
<script
type="text/javascript"
src={getAssetUrl('static/dwanalytics-22.2.js')}
id="dwanalytics"
async="async"
onLoad={trackPage}
></script>
)}
{ACTIVE_DATA_ENABLED && (
<script
src={getAssetUrl('static/dwac-21.7.js')}
type="text/javascript"
id="dwac"
async="async"
></script>
)}
</StorefrontPreview>
</Box>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/* eslint-disable no-import-assign */
import React from 'react'
import {screen, waitFor} from '@testing-library/react'
import {Helmet} from 'react-helmet'
Expand All @@ -15,18 +16,22 @@ import {DEFAULT_LOCALE} from '@salesforce/retail-react-app/app/utils/test-utils'
import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
import messages from '@salesforce/retail-react-app/app/static/translations/compiled/en-GB.json'
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
import * as constants from '@salesforce/retail-react-app/app/constants'

jest.mock('../../hooks/use-multi-site', () => jest.fn())

let windowSpy
let originalValue
beforeAll(() => {
jest.spyOn(console, 'log').mockImplementation(jest.fn())
jest.spyOn(console, 'groupCollapsed').mockImplementation(jest.fn())
originalValue = constants.ACTIVE_DATA_ENABLED
})

afterAll(() => {
console.log.mockRestore()
console.groupCollapsed.mockRestore()
constants.ACTIVE_DATA_ENABLED = originalValue
})
beforeEach(() => {
windowSpy = jest.spyOn(window, 'window', 'get')
Expand Down Expand Up @@ -67,6 +72,36 @@ describe('App', () => {
expect(screen.getByText('Any children here')).toBeInTheDocument()
})

test('Active Data component is not rendered', async () => {
constants.ACTIVE_DATA_ENABLED = false
useMultiSite.mockImplementation(() => resultUseMultiSite)
renderWithProviders(
<App targetLocale={DEFAULT_LOCALE} defaultLocale={DEFAULT_LOCALE} messages={messages}>
<p>Any children here</p>
</App>
)
await waitFor(() =>
expect(document.getElementById('headActiveData')).not.toBeInTheDocument()
)
await waitFor(() => expect(document.getElementById('dwanalytics')).not.toBeInTheDocument())
await waitFor(() => expect(document.getElementById('dwac')).not.toBeInTheDocument())
expect(screen.getByText('Any children here')).toBeInTheDocument()
})

test('Active Data component is rendered appropriately', async () => {
constants.ACTIVE_DATA_ENABLED = true
useMultiSite.mockImplementation(() => resultUseMultiSite)
renderWithProviders(
<App targetLocale={DEFAULT_LOCALE} defaultLocale={DEFAULT_LOCALE} messages={messages}>
<p>Any children here</p>
</App>
)
await waitFor(() => expect(document.getElementById('headActiveData')).toBeInTheDocument())
await waitFor(() => expect(document.getElementById('dwanalytics')).toBeInTheDocument())
await waitFor(() => expect(document.getElementById('dwac')).toBeInTheDocument())
expect(screen.getByText('Any children here')).toBeInTheDocument()
})

test('The localized hreflang links exist in the html head', () => {
useMultiSite.mockImplementation(() => resultUseMultiSite)
renderWithProviders(
Expand Down
3 changes: 3 additions & 0 deletions packages/template-retail-react-app/app/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,6 @@ export const SHIPPING_COUNTRY_CODES = [
{value: 'CA', label: 'Canada'},
{value: 'US', label: 'United States'}
]

// Constant to Enable Active Data
export const ACTIVE_DATA_ENABLED = false
96 changes: 96 additions & 0 deletions packages/template-retail-react-app/app/hooks/use-active-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/*global dw*/
import {ACTIVE_DATA_ENABLED} from '@salesforce/retail-react-app/app/constants'

const useActiveData = () => {
// Returns true when the feature flag is enabled and the tracking scripts have been executed
// This MUST be called before using the `dw` variable, otherwise a ReferenceError will be thrown
const canTrack = () => ACTIVE_DATA_ENABLED && typeof dw !== 'undefined'
return {
async sendViewProduct(category, product, type) {
if (!canTrack()) return
try {
if (dw?.ac) {
if (category && category.id) {
dw.ac.applyContext({category: category.id})
}
if (product && product.id) {
dw.ac._capture({id: product.id, type: type})
}
if (dw.ac?._scheduleDataSubmission) {
dw.ac._scheduleDataSubmission()
}
}
} catch (err) {
console.error(err)
}
},
async sendViewSearch(searchParams, productSearchResult) {
if (!canTrack()) return
try {
if (dw?.ac) {
dw.ac.applyContext({searchData: searchParams})
if (dw.ac?._scheduleDataSubmission) {
dw.ac._scheduleDataSubmission()
}
productSearchResult.hits.map((productSearchItem) => {
dw.ac._capture({id: productSearchItem.productId, type: 'searchhit'})
})
}
} catch (err) {
console.error(err)
}
},
async sendViewCategory(searchParams, category, productSearchResult) {
if (!canTrack()) return
try {
if (dw?.ac) {
if (category && category.id) {
dw.ac.applyContext({category: category.id, searchData: searchParams})
}
if (dw.ac?._scheduleDataSubmission) {
dw.ac._scheduleDataSubmission()
}
productSearchResult.hits.map((productSearchItem) => {
dw.ac._capture({id: productSearchItem.productId, type: 'searchhit'})
})
}
} catch (err) {
console.error(err)
}
},
async trackPage(siteId, localeId, currency) {
if (!canTrack()) return
try {
var activeDataUrl =
'/mobify/proxy/ocapi/on/demandware.store/Sites-' +
siteId +
'-Site/' +
localeId +
'/__Analytics-Start'
var dwAnalytics = dw.__dwAnalytics.getTracker(activeDataUrl)
if (typeof dw.ac == 'undefined') {
dwAnalytics.trackPageView()
} else {
try {
if (typeof dw.ac._setSiteCurrency === 'function') {
dw.ac._setSiteCurrency(currency)
}
} catch (err) {
console.error(err)
}
dw.ac.setDWAnalytics(dwAnalytics)
}
} catch (err) {
console.error(err)
}
}
}
}

export default useActiveData
106 changes: 106 additions & 0 deletions packages/template-retail-react-app/app/hooks/use-active-data.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/*global dw*/
/* eslint-disable no-import-assign */
/* eslint-disable react-hooks/rules-of-hooks */
import useActiveData from '@salesforce/retail-react-app/app/hooks/use-active-data'
import {
mockCategory,
mockProduct,
mockSearchResults
} from '@salesforce/retail-react-app/app/hooks/einstein-mock-data'
import * as constants from '@salesforce/retail-react-app/app/constants'
import {DEFAULT_SEARCH_PARAMS} from '@salesforce/retail-react-app/app/constants'

const activeDataApi = useActiveData()

beforeAll(() => {
window.dw = {
ac: {
applyContext: jest.fn(),
_capture: jest.fn(),
_scheduleDataSubmission: jest.fn(),
_setSiteCurrency: jest.fn(),
setDWAnalytics: jest.fn()
},
__dwAnalytics: {
getTracker: jest.fn()
}
}
})

afterAll(() => {
delete window.dw
})

describe('Test active data', () => {
let originalValue
beforeAll(() => (originalValue = constants.ACTIVE_DATA_ENABLED))
afterAll(() => (constants.ACTIVE_DATA_ENABLED = originalValue))
beforeEach(() => {
jest.resetAllMocks()
})

test('viewProduct captures expected product', async () => {
constants.ACTIVE_DATA_ENABLED = true
await activeDataApi.sendViewProduct(mockCategory, mockProduct, 'detail')
expect(dw.ac.applyContext).toHaveBeenCalledWith({category: mockCategory.id})
expect(dw.ac._capture).toHaveBeenCalledWith({id: mockProduct.id, type: 'detail'})
expect(dw.ac._scheduleDataSubmission).toHaveBeenCalledWith()
})

test('viewProduct does nothing', async () => {
constants.ACTIVE_DATA_ENABLED = false
await activeDataApi.sendViewProduct(mockCategory, mockProduct, 'detail')
expect(dw.ac.applyContext).toHaveBeenCalledTimes(0)
expect(dw.ac._capture).toHaveBeenCalledTimes(0)
expect(dw.ac._scheduleDataSubmission).toHaveBeenCalledTimes(0)
})

test('viewSearch applies search context and captures expected data', async () => {
constants.ACTIVE_DATA_ENABLED = true
await activeDataApi.sendViewSearch(DEFAULT_SEARCH_PARAMS, mockSearchResults)
expect(dw.ac.applyContext).toHaveBeenCalledWith({searchData: DEFAULT_SEARCH_PARAMS})
})

test('viewSearch does nothing', async () => {
constants.ACTIVE_DATA_ENABLED = false
await activeDataApi.sendViewSearch(DEFAULT_SEARCH_PARAMS, mockSearchResults)
expect(dw.ac.applyContext).toHaveBeenCalledTimes(0)
})

test('viewCategory applies category context and captures expected data', async () => {
constants.ACTIVE_DATA_ENABLED = true
await activeDataApi.sendViewCategory(DEFAULT_SEARCH_PARAMS, mockCategory, mockSearchResults)
expect(dw.ac.applyContext).toHaveBeenCalledWith({
category: mockCategory.id,
searchData: DEFAULT_SEARCH_PARAMS
})
expect(dw.ac._scheduleDataSubmission).toHaveBeenCalledWith()
})

test('viewCategory does nothing', async () => {
constants.ACTIVE_DATA_ENABLED = false
await activeDataApi.sendViewCategory(DEFAULT_SEARCH_PARAMS, mockCategory, mockSearchResults)
expect(dw.ac.applyContext).toHaveBeenCalledTimes(0)
expect(dw.ac._scheduleDataSubmission).toHaveBeenCalledTimes(0)
})

test('trackPage sets expected DW analytics', async () => {
constants.ACTIVE_DATA_ENABLED = true
await activeDataApi.trackPage('test-site-id', 'en-US', 'USD')
expect(dw.__dwAnalytics.getTracker).toHaveBeenCalledWith(
'/mobify/proxy/ocapi/on/demandware.store/Sites-test-site-id-Site/en-US/__Analytics-Start'
)
})

test('trackPage does nothing', async () => {
constants.ACTIVE_DATA_ENABLED = false
await activeDataApi.trackPage('test-site-id', 'en-US', 'USD')
expect(dw.__dwAnalytics.getTracker).toHaveBeenCalledTimes(0)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-curre
import {useVariant} from '@salesforce/retail-react-app/app/hooks'
import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
import useActiveData from '@salesforce/retail-react-app/app/hooks/use-active-data'
import {useServerContext} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
// Project Components
import RecommendedProducts from '@salesforce/retail-react-app/app/components/recommended-products'
Expand All @@ -51,6 +52,7 @@ const ProductDetail = () => {
const history = useHistory()
const location = useLocation()
const einstein = useEinstein()
const activeData = useActiveData()
const toast = useToast()
const navigate = useNavigation()
const [productSetSelection, setProductSetSelection] = useState({})
Expand Down Expand Up @@ -272,10 +274,20 @@ const ProductDetail = () => {
einstein.sendViewProduct(product)
const childrenProducts = product.setProducts
childrenProducts.map((child) => {
einstein.sendViewProduct(child)
try {
einstein.sendViewProduct(child)
} catch (err) {
console.error(err)
}
activeData.sendViewProduct(category, child, 'detail')
})
} else if (product) {
einstein.sendViewProduct(product)
try {
einstein.sendViewProduct(product)
} catch (err) {
console.error(err)
}
activeData.sendViewProduct(category, product, 'detail')
}
}, [product])

Expand Down
Loading