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

Shopify analytics 2 #1325

Merged
merged 67 commits into from
Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
5ac8f88
migrate shopify analytic code from old branch
wizardlyhel May 19, 2022
4e4f357
working page view
wizardlyhel May 20, 2022
549fe32
revert some entry changes
wizardlyhel May 20, 2022
7f15d0f
merge with v1.x-2022-07
wizardlyhel May 20, 2022
da764aa
clean up and put back some fix
wizardlyhel May 20, 2022
2f843ff
instrumented analytics
wizardlyhel May 20, 2022
1a09d08
fix server side analytics
wizardlyhel May 20, 2022
be26143
more page types
wizardlyhel May 20, 2022
d584497
add shopify analytics constants
wizardlyhel May 24, 2022
bb3fa18
merge with v1.x-2022-07
wizardlyhel May 24, 2022
9de0de1
move currency query to template
wizardlyhel May 24, 2022
8bb20d2
fix analytics event errors
wizardlyhel May 25, 2022
bdf1f0b
update node typescript version
wizardlyhel May 25, 2022
8314820
restore yarn lock changes
wizardlyhel May 25, 2022
f31490f
ts error
wizardlyhel May 25, 2022
29a8d15
add more fields
wizardlyhel May 25, 2022
e7864bf
match oxygen domain
wizardlyhel May 25, 2022
e68e37a
change const name so it isn't confusing
wizardlyhel May 25, 2022
5b05304
fix cookie setting
wizardlyhel May 25, 2022
5268b12
merge with v1.x-2022-07
wizardlyhel May 26, 2022
e8bb405
conditional oxygen variable
wizardlyhel May 26, 2022
eefc90f
add changeset and fix react error
wizardlyhel May 26, 2022
bd79723
fix missing import
wizardlyhel May 26, 2022
1a257ca
add shopify analytics doc
wizardlyhel May 26, 2022
8e1f80d
clean up
wizardlyhel May 26, 2022
f80f0aa
fix test
wizardlyhel May 26, 2022
c5d02c6
fix test 2
wizardlyhel May 26, 2022
82dc942
fix a bad merge
wizardlyhel May 26, 2022
da18ce1
clean up
wizardlyhel May 27, 2022
7ddeac3
add test
wizardlyhel May 27, 2022
ddd7314
remove console log
wizardlyhel May 27, 2022
3e4b46c
remove the need to specify cookie name
wizardlyhel May 30, 2022
af6e2d1
merge with v1.x-2022-07
wizardlyhel May 30, 2022
32a6d58
remove the need to use session api
wizardlyhel May 30, 2022
d65d8c2
revert session api changes
wizardlyhel May 30, 2022
12b0da5
merage with v.1x-2022-07
wizardlyhel May 30, 2022
2b8a6d2
clean up
wizardlyhel May 30, 2022
490e41d
update doc
wizardlyhel May 30, 2022
3bd1d5d
more clean up
wizardlyhel May 30, 2022
a61d87c
prettier
wizardlyhel May 30, 2022
a9ba5ec
update doc
wizardlyhel May 31, 2022
cdd13d5
update doc 2
wizardlyhel May 31, 2022
33621b5
match analytics schema
wizardlyhel May 31, 2022
d087fe5
Update .changeset/curly-phones-search.md
wizardlyhel May 31, 2022
d6105c6
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
87bab2e
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
829b27b
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
e37bc0b
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
9f56514
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
b966253
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
ad9eb06
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
5b9e1d3
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
a7f1aac
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
906bdd6
add work in progress disclosure
wizardlyhel May 31, 2022
476a6c1
merge
wizardlyhel May 31, 2022
7921d1d
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
4b02072
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
0674fdd
Update docs/components/framework/shopifyanalytics.md
wizardlyhel May 31, 2022
9c3b234
add related doc
wizardlyhel May 31, 2022
817de86
address feedback
wizardlyhel May 31, 2022
5ae8b85
use wrapPromise
wizardlyhel May 31, 2022
c7ea791
log error
wizardlyhel May 31, 2022
84ea431
add to cart event
wizardlyhel Jun 6, 2022
c142b58
use schema 1.2
wizardlyhel Jun 6, 2022
f6a4da4
remove the add to cart work
wizardlyhel Jun 7, 2022
c7218df
merge v1.x-20022-07
wizardlyhel Jun 7, 2022
a4d2fbc
make sure the app id is a string
wizardlyhel Jun 7, 2022
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
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).
wizardlyhel marked this conversation as resolved.
Show resolved Hide resolved

> 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" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we want to expose other cookie settings?

Copy link
Collaborator Author

@wizardlyhel wizardlyhel May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

  • maxage: Defined by Shopify to be 2 years for _shopify_y and 30 mins for _shopify_s
  • path: should always be /

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might change when CHIP is fully supported

```
wizardlyhel marked this conversation as resolved.
Show resolved Hide resolved

{% 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make this change? The request object itself has both the url and headers already on it. And it's more flexible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request object is garbage collected after we return a response. This was fine when there was only 1 server analytics connector. When there are more than 1 server analytics connector, we get into situation where the request no longer exist for the multiple server analytics connectors to process

// 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
4 changes: 4 additions & 0 deletions packages/hydrogen/src/components/CartProvider/cart-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,17 @@ fragment CartFragment on Cart {
...ImageFragment
}
product {
id
handle
title
productType
vendor
}
selectedOptions {
name
value
}
sku
}
}
}
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