Skip to content

Commit

Permalink
Shopify analytics 2 (#1325)
Browse files Browse the repository at this point in the history
* Instrumented page view and make sure Shopfiy live view works

Co-authored-by: Michelle Vinci <[email protected]>
  • Loading branch information
wizardlyhel and mcvinci authored Jun 7, 2022
1 parent 0b4ee48 commit 572c18d
Show file tree
Hide file tree
Showing 30 changed files with 1,027 additions and 94 deletions.
31 changes: 31 additions & 0 deletions .changeset/curly-phones-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@shopify/hydrogen': minor
'template-hydrogen-default': minor
---

- Fix clientAnalytics not waiting for all server analytics data before sending page view event
- Fix server analytics connector erroring out after more than 1 server analytics connectors are attached
- Shopify analytics components

# Updates to server analytics connectors

The server analytics connector interface has updated to

```jsx
export function request(
requestUrl: string,
requestHeader: Headers,
data?: any,
contentType?: string
): void {
// Do something with the analytic event
}
```

# Introducing Shopify analytics

Optional analytics components that allows you to send ecommerce related analytics to
Shopify. Adding the Shopify analytics components will allow the Shopify admin - Analytics
dashboard to work.

For information, see [Shopify Analytics](https://shopify.dev/api/hydrogen/components/framework/shopifyanalytics)
128 changes: 128 additions & 0 deletions docs/components/framework/shopifyanalytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
gid: 398e9916-c99c-4d92-af5c-6681dd5e37d5
title: ShopifyAnalytics
description: The ShopifyAnalytics component sends commerce-related analytics to Shopify.
---

The `ShopifyAnalytics` component sends commerce-related analytics to Shopify. By adding the `ShopifyAnalytics` component to your Hydrogen app, you can view key sales, orders, and online store visitor data from the [Analytics dashboard in your Shopify admin](https://help.shopify.com/en/manual/reports-and-analytics/shopify-reports/overview-dashboard).

> Note:
> Currently, only Online Store page view and session-related analytic reports can be configured with the `ShopifyAnalytics` component. Additional analytics functionality will be available in the coming weeks.
## Configuration

To set up the `ShopifyAnalytics` component in your Hydrogen app, add the `ShopifyAnalyticsServerConnector` property to your [Hydrogen configuration file](https://shopify.dev/custom-storefronts/hydrogen/framework/hydrogen-config):

{% codeblock file, filename: 'hydrogen.config.js' %}

```jsx
import {
...
ShopifyAnalyticsServerConnector,
} from '@shopify/hydrogen';
...
export default defineConfig({
...
serverAnalyticsConnectors: [
PerformanceMetricsServerAnalyticsConnector,
// The `serverAnalyticsConnectors` property allows you to send analytics data from the server in your Hydrogen app.
ShopifyAnalyticsServerConnector,
],
});
```

{% endcodeblock %}

After you've updated your Hydrogen configuration file, add the `ShopifyAnalytics` component in `App.server.jsx`.

{% codeblock file, filename: 'App.server.jsx' %}

```jsx
function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<ShopifyProvider>
...
<ShopifyAnalytics />
</ShopifyProvider>
</Suspense>
);
}
```

{% endcodeblock %}

If you have a custom domain or you're using sub-domains, then you can set the cookie domain of
the `ShopifyAnalytics` component so that cookies persists for your root domain:

{% codeblock file, filename: 'App.server.jsx' %}

```jsx
<ShopifyAnalytics cookieDomain="my-shop.com" />
```

{% endcodeblock %}

If you're not using custom domains or sub-domains, then the `ShopifyAnalytics` component uses the `storeDomain` value in the Hydrogen configuration file as the default cookie domain or leaves it blank when the specified cookie domain doesn't match `window.location.hostname`.

### Connecting Hydrogen analytics with Shopify checkout

Analytic cookies must be set at the first-party domain. This means that when a buyer navigates from your Hydrogen storefront to Shopify checkout, the domain name must stay the same.

You can achieve this by assigning a sub-domain to your online store. For example, you can do the following tasks:

- Set your Hydrogen store domain to `https://www.my-awesome-hydrogen-store.com`.
- Attach a new sub-domain to your online store at `https://checkout.my-awesome-hydrogen-store.com`.
- Set the `cookieDomain` to the same root domain at `<ShopifyAnalytics cookieDomain="my-awesome-hydrogen-store.com" />`.

> Note:
> Hydrogen analytics and Shopify checkout can only be connected in production. They can't be connected in development and preview modes.
## Shopify Analytics data

Provide the following data to `useServerAnalytics` to view information from the Analytics dashboard in your Shopify admin:

| Prop | Description | Example code |
| -------- | ------------------- | ------------------- |
| shopId | The ID of your Shopify store. | [DefaultSeo.server.jsx](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/components/DefaultSeo.server.jsx) |
| currency | The currency being presented to the buyer on the webpage. | [DefaultSeo.server.jsx](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/components/DefaultSeo.server.jsx) |
| pageType? | The page template type for your routes. For a list of valid values, refer to [ShopifyAnalytics constants](#shopifyanalytics-constants). | [collections/[handle].server.jsx](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/collections/%5Bhandle%5D.server.jsx) |
| resourceId? | The ID of the page template type for the routes that use Shopify resources. <br></br>This only applies to the following routes: `article`, `blog`, `collection`, `page`, `product`. | [products/[handle].server.jsx](https://github.com/Shopify/hydrogen/blob/main/templates/template-hydrogen-default/src/routes/products/%5Bhandle%5D.server.jsx) |

### `ShopifyAnalytics` constants

The following table provides a list of valid values for the `pageType` property:

| Value | Description |
| -------- | --------------------- |
| `article` | A page that displays an article in an online store blog. |
| `blog` | A page that displays an online store blog. |
| `captcha` | A page that uses Google's [reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) to help prevent spam through customer, contact, and blog comment forms. |
| `cart` | A page that displays the merchandise that a buyer intends to purchase, and the estimated cost associated with the cart. |
| `collection` | A page that displays a grouping of products. |
| `customersAccount` | A page that provides details about a customer's account. |
| `customersActivateAccount` | A page that enables a customer to activate their account. |
| `customersAddresses` | A page that displays a customer's addresses. |
| `customersLogin` | A page that enables a customer to log in to a storefront. |
| `customersOrder` | A page that displays a customer's orders. |
| `customersRegister` | A page that enables a customer to create and register their account. |
| `customersResetPassword` | A page that enables a customer to reset the password associated with their account. |
| `giftCard` | A page that displays an issued gift card. |
| `home` | The homepage of the online store. |
| `listCollections` | A page that displays a list of collections, which each contain a grouping of products. |
| `forbidden` | A page that users can't access due to insufficient permissions. |
| `notFound` | A page no longer exists or is inaccessible. |
| `page` | A page that holds static HTML content. Each `page` object represents a custom page on the online store. |
| `password` | A page that's shown when [password protection is applied to the store](https://help.shopify.com/en/manual/online-store/themes/password-page). |
| `product` | A page that represents an individual item for sale in a store. |
| `policy` | A page that provides the storefront's policy. |
| `search` | A page that displays the results of a [storefront search](https://help.shopify.com/en/manual/online-store/storefront-search). |

## Component type

The `ShopifyAnalytics` component is a server component, which means that it renders on the server. For more information about component types, refer to [React Server Components](https://shopify.dev/custom-storefronts/hydrogen/framework/react-server-components).

## Related framework topics

- [Analytics](https://shopify.dev/custom-storefronts/hydrogen/framework/analytics)
- [Session management](https://shopify.dev/custom-storefronts/hydrogen/framework/sessions)
17 changes: 11 additions & 6 deletions docs/framework/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ To send analytics data from the server-side, complete the following steps:
{% codeblock file, filename: 'MyServerAnalyticsConnector.jsx' %}
```jsx
export function request(request, data, contentType) {
export function request(requestUrl, requestHeader, data, contentType) {
// Send your analytics request to third-party analytics
}
```
Expand All @@ -283,11 +283,12 @@ To send analytics data from the server-side, complete the following steps:
The following table describes the request function parameters for `ServerAnalyticsConnector`:
| Parameter | Type | Description |
| ------------- | -------------- | ------------------------------------------------- |
| `request` | request | The analytics request object. |
| `data` | object or text | The result from `.json()` or `.text()`. |
| `contentType` | string | The content type. Valid values: `json` or `text`. |
| Parameter | Type | Description |
| --------------- | -------------- | ------------------------------------------------- |
| `requestUrl` | string | The analytics request url. |
| `requestHeader` | Headers | The analytics request headers object. |
| `data` | object or text | The result from `.json()` or `.text()`. |
| `contentType` | string | The content type. Valid values: `json` or `text`. |
## Unsubscribe from an event
Expand Down Expand Up @@ -486,6 +487,10 @@ describe('Google Analytics 4', () => {

{% endcodeblock %}

## Related components

- [`ShopifyAnalytics`](https://shopify.dev/api/hydrogen/components/framework/shopifyanalytics)

## Next steps

- Learn how to [configure queries to preload](https://shopify.dev/custom-storefronts/hydrogen/framework/preloaded-queries) in your Hydrogen app.
Expand Down
33 changes: 16 additions & 17 deletions packages/hydrogen/src/foundation/Analytics/Analytics.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,12 @@ export function Analytics({
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);

if (urlParams.has('utm_source')) {
ClientAnalytics.pushToPageAnalyticsData(
{
id: urlParams.get('utm_id'),
source: urlParams.get('utm_source'),
campaign: urlParams.get('utm_campaign'),
medium: urlParams.get('utm_medium'),
content: urlParams.get('utm_content'),
term: urlParams.get('utm_term'),
},
'utm'
);
}
addUTMData(urlParams, 'id');
addUTMData(urlParams, 'source');
addUTMData(urlParams, 'campaign');
addUTMData(urlParams, 'medium');
addUTMData(urlParams, 'content');
addUTMData(urlParams, 'term');

ClientAnalytics.pushToPageAnalyticsData(analyticsDataFromServer);
ClientAnalytics.publish(ClientAnalytics.eventNames.PAGE_VIEW, true);
Expand All @@ -32,11 +25,17 @@ export function Analytics({
}
);
}

return function cleanup() {
ClientAnalytics.resetPageAnalyticsData();
};
}, [analyticsDataFromServer]);

return null;
}

function addUTMData(urlParams: URLSearchParams, key: string) {
if (urlParams.has(`utm_${key}`)) {
ClientAnalytics.pushToPageAnalyticsData({
utm: {
[key]: urlParams.get(`utm_${key}`),
},
});
}
}
59 changes: 36 additions & 23 deletions packages/hydrogen/src/foundation/Analytics/Analytics.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,23 @@ import {useServerAnalytics} from './hook';
import {Analytics as AnalyticsClient} from './Analytics.client';
import {useServerRequest} from '../ServerRequestProvider';
import AnalyticsErrorBoundary from '../AnalyticsErrorBoundary.client';
import {wrapPromise} from '../../utilities';

const DELAY_KEY = 'analytics-delay';
const DELAY_KEY_1 = 'analytics-delay-1';
const DELAY_KEY_2 = 'analytics-delay-2';

export function Analytics() {
const cache = useServerRequest().ctx.cache;

// If render cache is empty, create a 50 ms delay so that React doesn't resolve this
// component too early and potentially cause a mismatch in hydration
if (cache.size === 0 && !cache.has(DELAY_KEY)) {
let result: boolean;
let promise: Promise<boolean>;

cache.set(DELAY_KEY, () => {
if (result !== undefined) {
return result;
}

if (!promise) {
promise = new Promise((resolve) => {
setTimeout(() => {
result = true;
resolve(true);
}, 50);
});
}

throw promise;
});
if (cache.size === 0 && !cache.has(DELAY_KEY_1)) {
analyticsDelay(cache, DELAY_KEY_1, 50);
}
// If this delay is created, execute it
cache.has(DELAY_KEY_1) && cache.get(DELAY_KEY_1).read();
// clean up this key so that it won't be saved to the preload cache
cache.delete(DELAY_KEY_1);

// Make sure all queries have returned before rendering the Analytics server component
cache.forEach((cacheFn: any) => {
Expand All @@ -41,10 +29,35 @@ export function Analytics() {
}
});

const analyticsData = useServerAnalytics();
// If all queries has returned (could be from cached queries),
// delay Analytic component by another 1ms (put this component
// to the end of the render queue) so that other scheduled
// render work can be processed by React's concurrent render first
if (cache.size > 1 && !cache.has(DELAY_KEY_2)) {
analyticsDelay(cache, DELAY_KEY_2, 1);
}
cache.has(DELAY_KEY_2) && cache.get(DELAY_KEY_2).read();
cache.delete(DELAY_KEY_2);

return (
<AnalyticsErrorBoundary>
<AnalyticsClient analyticsDataFromServer={analyticsData} />
<AnalyticsClient analyticsDataFromServer={useServerAnalytics()} />
</AnalyticsErrorBoundary>
);
}

function analyticsDelay(
cache: Map<string, any>,
delayKey: string,
delay: number
) {
const delayPromise = wrapPromise(
new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, delay);
})
);

cache.set(delayKey, delayPromise);
}
Loading

0 comments on commit 572c18d

Please sign in to comment.