Merge pull request #32944 from woocommerce/add/tracks-experimental-products

Add tracks for experimental products page
This commit is contained in:
RJ 2022-05-13 12:41:48 +08:00 committed by GitHub
commit 2c5f47a91d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 284 additions and 18 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: Add
Changed task_view experimental_product key to variant (technically a breaking change but since it was introduced in the same version it is fine) #32944

View File

@ -8,7 +8,7 @@ import { Slot, Fill } from '@wordpress/components';
/**
* Internal dependencies
*/
import { isProductTaskExperimentTreatment } from './use-product-layout-experiment';
import { getProductLayoutExperiment } from './use-product-layout-experiment';
export const trackView = async ( taskId ) => {
const activePlugins = wp.data
@ -25,9 +25,7 @@ export const trackView = async ( taskId ) => {
recordEvent( 'task_view', {
task_name: taskId,
experimental_products:
window.wcAdminFeatures[ 'experimental-products-task' ] &&
( await isProductTaskExperimentTreatment() ),
variant: await getProductLayoutExperiment(),
wcs_installed: installedPlugins.includes( 'woocommerce-services' ),
wcs_active: activePlugins.includes( 'woocommerce-services' ),
jetpack_installed: installedPlugins.includes( 'jetpack' ),

View File

@ -7,6 +7,12 @@ import apiFetch from '@wordpress/api-fetch';
import { WC_ADMIN_NAMESPACE } from '@woocommerce/data';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import useRecordCompletionTime from '../use-record-completion-time';
type UseLoadSampleProductsProps = {
redirectUrlAfterSuccess: string;
@ -17,8 +23,13 @@ const useLoadSampleProducts = ( {
}: UseLoadSampleProductsProps ) => {
const [ isRequesting, setIsRequesting ] = useState< boolean >( false );
const { createNotice } = useDispatch( 'core/notices' );
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
const loadSampleProduct = async () => {
recordEvent( 'tasklist_add_product', {
method: 'sample_product',
} );
recordCompletionTime();
setIsRequesting( true );
try {
await apiFetch( {

View File

@ -50,6 +50,7 @@ export const Products = () => {
const productTypeListItems = useProductTypeListItems(
getProductTypes( [ 'subscription' ] ),
[],
{
onClick: recordCompletionTime,
}

View File

@ -73,6 +73,8 @@ describe( 'Products', () => {
now: jest
.fn()
.mockReturnValueOnce( 0 )
.mockReturnValueOnce( 1000 )
.mockReturnValueOnce( 0 )
.mockReturnValueOnce( 1000 ),
},
} );
@ -84,14 +86,21 @@ describe( 'Products', () => {
'FROM A CSV FILE Import all products at once by uploading a CSV file.',
} )
);
await waitFor( () =>
expect( recordEvent ).toHaveBeenCalledWith(
await waitFor( () => {
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'import' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{
task_name: 'products',
time: '0-2s',
}
)
);
} );
} );
} );

View File

@ -6,6 +6,7 @@ import { Text } from '@woocommerce/experimental';
import { Link } from '@woocommerce/components';
import interpolateComponents from '@automattic/interpolate-components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
@ -13,6 +14,7 @@ import { getAdminLink } from '@woocommerce/settings';
import { ProductType } from './constants';
import CardList from '../experimental-import-products/CardList';
import './card-layout.scss';
import useRecordCompletionTime from '../use-record-completion-time';
type CardProps = {
items: ( ProductType & {
@ -21,6 +23,8 @@ type CardProps = {
};
const CardLayout: React.FC< CardProps > = ( { items } ) => {
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
return (
<div className="woocommerce-products-card-layout">
<Text className="woocommerce-products-card-layout__description">
@ -32,6 +36,10 @@ const CardLayout: React.FC< CardProps > = ( { items } ) => {
sbLink: (
<Link
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'manually',
} );
recordCompletionTime();
window.location = getAdminLink(
'post-new.php?post_type=product&wc_onboarding_active_task=products&tutorial=true'
);

View File

@ -7,8 +7,16 @@ import { Text } from '@woocommerce/experimental';
import { ExternalLink } from '@wordpress/components';
import { Link } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import useRecordCompletionTime from '../use-record-completion-time';
const Footer: React.FC = () => {
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
return (
<div className="woocommerce-products-footer">
<Text className="woocommerce-products-footer__selling-somewhere-else">
@ -23,6 +31,10 @@ const Footer: React.FC = () => {
importCSVLink: (
<Link
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'import',
} );
recordCompletionTime();
window.location = getAdminLink(
'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=products'
);
@ -36,6 +48,12 @@ const Footer: React.FC = () => {
),
_3rdLink: (
<ExternalLink
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'migrate',
} );
recordCompletionTime();
} }
href="https://woocommerce.com/products/cart2cart/?utm_medium=product"
type="external"
>

View File

@ -26,6 +26,7 @@ import CardLayout from './card-layout';
import { LoadSampleProductType } from './constants';
import LoadSampleProductModal from '../components/load-sample-product-modal';
import useLoadSampleProducts from '../components/use-load-sample-products';
import useRecordCompletionTime from '../use-record-completion-time';
const getOnboardingProductType = (): string[] => {
const onboardingData = getAdminSetting( 'onboarding' );
@ -57,11 +58,28 @@ export const Products = () => {
experimentLayout,
] = useProductTaskExperiment();
const productTypes = useProductTypeListItems( getProductTypes() );
const surfacedProductTypeKeys = getSurfacedProductTypeKeys(
getOnboardingProductType()
);
const productTypes = useProductTypeListItems(
getProductTypes(),
surfacedProductTypeKeys
);
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
const productTypesWithTimeRecord = useMemo(
() =>
productTypes.map( ( productType ) => ( {
...productType,
onClick: () => {
productType.onClick();
recordCompletionTime();
},
} ) ),
[ recordCompletionTime ]
);
const {
loadSampleProduct,
isLoadingSampleProducts,
@ -72,12 +90,13 @@ export const Products = () => {
} );
const visibleProductTypes = useMemo( () => {
const surfacedProductTypes = productTypes.filter( ( productType ) =>
const surfacedProductTypes = productTypesWithTimeRecord.filter(
( productType ) =>
surfacedProductTypeKeys.includes( productType.key )
);
if ( isExpanded ) {
// To show product types in same order, we need to push the other product types to the end.
productTypes.forEach(
productTypesWithTimeRecord.forEach(
( productType ) =>
! surfacedProductTypes.includes( productType ) &&
surfacedProductTypes.push( productType )

View File

@ -6,12 +6,14 @@ import { List, Link } from '@woocommerce/components';
import { Text } from '@woocommerce/experimental';
import interpolateComponents from '@automattic/interpolate-components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { ProductType } from './constants';
import './stack.scss';
import useRecordCompletionTime from '../use-record-completion-time';
type StackProps = {
items: ( ProductType & {
@ -26,6 +28,8 @@ const Stack: React.FC< StackProps > = ( {
onClickLoadSampleProduct,
showOtherOptions = true,
} ) => {
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
return (
<div className="woocommerce-products-stack">
<List items={ items } />
@ -40,6 +44,10 @@ const Stack: React.FC< StackProps > = ( {
sbLink: (
<Link
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'manually',
} );
recordCompletionTime();
window.location = getAdminLink(
'post-new.php?post_type=product&wc_onboarding_active_task=products&tutorial=true'
);
@ -56,6 +64,7 @@ const Stack: React.FC< StackProps > = ( {
href=""
type="wc-admin"
onClick={ () => {
recordCompletionTime();
onClickLoadSampleProduct();
return false;
} }

View File

@ -1,7 +1,8 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
@ -9,7 +10,11 @@ import { render } from '@testing-library/react';
import CardLayout from '../card-layout';
import { productTypes } from '../constants';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
describe( 'CardLayout', () => {
beforeEach( () => {
( recordEvent as jest.Mock ).mockClear();
} );
it( 'should render all products types in CardLayout', () => {
const { queryByText, queryAllByRole } = render(
<CardLayout
@ -26,4 +31,28 @@ describe( 'CardLayout', () => {
expect( queryAllByRole( 'link' ) ).toHaveLength( 1 );
} );
it( 'start blank link should fire the tasklist_add_product and completion events', () => {
const { getByText } = render(
<CardLayout
items={ [
{
...productTypes[ 0 ],
onClick: () => {},
},
] }
/>
);
fireEvent.click( getByText( 'Start blank' ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'manually' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
} );

View File

@ -2,6 +2,10 @@
* External dependencies
*/
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { recordEvent } from '@woocommerce/tracks';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
/**
* Internal dependencies
@ -9,8 +13,41 @@ import { render } from '@testing-library/react';
import Footer from '../footer';
describe( 'Footer', () => {
beforeEach( () => {
( recordEvent as jest.Mock ).mockClear();
} );
it( 'should render footer with two links', () => {
const { queryAllByRole } = render( <Footer /> );
expect( queryAllByRole( 'link' ) ).toHaveLength( 2 );
} );
it( 'clicking on import CSV should fire event tasklist_add_product with method:import and task_completion_time', () => {
const { getByText } = render( <Footer /> );
userEvent.click( getByText( 'Import your products from a CSV file' ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'import' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
it( 'clicking on start blank should fire event tasklist_add_product with method:migrate and task_completion_time', () => {
const { getByText } = render( <Footer /> );
userEvent.click( getByText( 'use a 3rd party migration plugin' ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'migrate' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
} );

View File

@ -4,6 +4,7 @@
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useProductTaskExperiment } from '@woocommerce/onboarding';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
@ -25,6 +26,12 @@ jest.mock( '@woocommerce/onboarding', () => ( {
useProductTaskExperiment: jest.fn().mockReturnValue( [ false, 'stacked' ] ),
} ) );
jest.mock( '../use-create-product-by-type', () => ( {
useCreateProductByType: jest
.fn()
.mockReturnValue( { createProductByType: jest.fn() } ),
} ) );
global.fetch = jest.fn().mockImplementation( () =>
Promise.resolve( {
json: () => Promise.resolve( {} ),
@ -32,6 +39,8 @@ global.fetch = jest.fn().mockImplementation( () =>
} )
);
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
describe( 'Products', () => {
beforeEach( () => {
jest.clearAllMocks();
@ -65,6 +74,70 @@ describe( 'Products', () => {
expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
} );
it( 'clicking on suggested product should fire event tasklist_product_template_selection with is_suggested:true and task_completion_time', () => {
( getAdminSetting as jest.Mock ).mockImplementation( () => ( {
profile: {
product_types: [ 'downloads' ],
},
} ) );
const { getByRole } = render( <Products /> );
userEvent.click(
getByRole( 'menuitem', {
name:
'Digital product A digital product like service, downloadable book, music or video.',
} )
);
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_product_template_selection',
{ is_suggested: true, product_type: 'digital' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
it( 'clicking on not-suggested product should fire event tasklist_product_template_selection with is_suggested:false and task_completion_time', async () => {
( getAdminSetting as jest.Mock ).mockImplementation( () => ( {
profile: {
product_types: [ 'downloads' ],
},
} ) );
const { queryByText, getByRole, queryByRole } = render( <Products /> );
expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
userEvent.click(
getByRole( 'button', { name: 'View more product types' } )
);
await waitFor( () =>
expect( queryByRole( 'menu' )?.childElementCount ).toBe(
productTypes.length
)
);
userEvent.click(
getByRole( 'menuitem', {
name: 'Grouped product A collection of related products.',
} )
);
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_product_template_selection',
{ is_suggested: false, product_type: 'grouped' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
it( 'should render all products type when clicking view more button', async () => {
( getAdminSetting as jest.Mock ).mockImplementation( () => ( {
profile: {

View File

@ -3,6 +3,7 @@
*/
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
@ -10,7 +11,13 @@ import userEvent from '@testing-library/user-event';
import Stack from '../stack';
import { productTypes } from '../constants';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
describe( 'Stack', () => {
beforeEach( () => {
( recordEvent as jest.Mock ).mockClear();
} );
it( 'should render stack with given product type and two links', () => {
const { queryByText, queryAllByRole } = render(
<Stack
@ -63,5 +70,38 @@ describe( 'Stack', () => {
getByRole( 'link', { name: 'Load Sample Products' } )
);
await waitFor( () => expect( onClickLoadSampleProduct ).toBeCalled() );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
it( 'should fire the tasklist_add_product and task_completion_time events when the "Start Blank" link is clicked', async () => {
const onClickLoadSampleProduct = jest.fn();
const { getByRole } = render(
<Stack
onClickLoadSampleProduct={ onClickLoadSampleProduct }
items={ [
{
...productTypes[ 0 ],
onClick: () => {},
},
] }
/>
);
userEvent.click( getByRole( 'link', { name: 'Start Blank' } ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'manually' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
} );

View File

@ -12,7 +12,7 @@ import { useState } from '@wordpress/element';
import { ProductTypeKey } from './constants';
import { createNoticesFromResponse } from '../../../lib/notices';
const useCreateProductByType = () => {
export const useCreateProductByType = () => {
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );
const [ isRequesting, setIsRequesting ] = useState< boolean >( false );
@ -54,5 +54,3 @@ const useCreateProductByType = () => {
isRequesting,
};
};
export default useCreateProductByType;

View File

@ -2,15 +2,17 @@
* External dependencies
*/
import { useMemo } from '@wordpress/element';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import useCreateProductByType from './use-create-product-by-type';
import { ProductType } from './constants';
import { useCreateProductByType } from './use-create-product-by-type';
import { ProductType, ProductTypeKey } from './constants';
const useProductTypeListItems = (
_productTypes: ProductType[],
suggestedProductTypes: ProductTypeKey[] = [],
{
onClick,
}: {
@ -25,6 +27,12 @@ const useProductTypeListItems = (
...productType,
onClick: () => {
createProductByType( productType.key );
recordEvent( 'tasklist_product_template_selection', {
product_type: productType.key,
is_suggested: suggestedProductTypes.includes(
productType.key
),
} );
if ( typeof onClick === 'function' ) {
onClick();
}

View File

@ -0,0 +1,4 @@
Significance: patch
Type: Add
Added events for experimental products page #32944