Technical Blog

8 Posts tagged with the storefront tag

The performance of the OOTB CachingStoreResolver begins to degrade when more than a handful of stores are configured in EP. This post describes a simple replacement implementation that scales much better.

 

The problem

 

The StoreResolver is used by the storefront to resolve stores by either store code or domain. The OOTB CachingStoreResolver uses a SimpleTimeoutCache to save store codes and domain-to-store mappings. The default cache timeout is 1 minute. When an entry is not found in the cache, the request is delegated to the StoreResolver to resolve the request from the database and the result is cached if valid. All this looks pretty reasonable on the surface.

 

The scalability problems are caused by how the cache is populated. If a store code or domain are not found in cache, then the StoreResolver reads ALL stores from the database and then returns only the matching store code. If there are 100 stores and the default cache timeout is used then we can expect cache entries to expire nearly twice every second. While this is not ideal, there wouldn't be much of an issue if reading a store were a lightweight query. However OpenJPA brings along a large number of related objects and the operation ends up being quite expensive.

 

It is also worth noting that a request with an invalid store code or domain will always have a cache miss and will result in fetching all stores from the database.

 

Two solutions

 

The simplest approach is to increase the cache timeout, but this does not fix the issue of handling invalid store codes or domains.

 

A more complete solution is to replace the CachingStoreResolver with the following class which caches all store codes or domain-to-store mappings with a single query.

 

import java.util.Collection;
import java.util.Map;

import com.elasticpath.sfweb.util.impl.StoreResolverImpl;

 

/**
* This re-implements the OOTB CachingStoreResolver to cache all stores in a single database query.
*/
public class ReplacementCachingStoreResolverImpl extends StoreResolverImpl {

 

    /**
     * Get the set of valid store codes.
     *
     * @return a collection of all codes for complete stores.
     */
    @Override
    protected Collection<String> getStoreCodes() {
        final String cacheKey = "storeCodes";
        Collection<String> storeCodes = (Collection<String>) getFromCache(cacheKey);
        if (storeCodes == null) {
            storeCodes = super.getStoreCodes();
            addToCache(cacheKey, storeCodes);
        }
        return storeCodes;
    }

 

    /**
     * Creates a map with all the store domains mapped to their store codes.
     *
     * @return a map populated with all store domains mapped to
     *            their corresponding store codes.
     */
    @Override
    protected Map<String, String> getDomainToStoreMapping() {
        final String cacheKey = "domainToStoreMap";
        Map<String, String> storeCodeMap = (Map<String, String>) getFromCache(cacheKey);
        if (storeCodeMap == null) {
            storeCodeMap = super.getDomainToStoreMapping();
            addToCache(cacheKey, storeCodeMap);
        }
        return storeCodeMap;
    }

 

    private Object getFromCache(final String cacheKey) {
        // TODO - Hook in your cache of choice, could be a SimpleTimeoutCache
        return null;
    }
   
    private void addToCache(final String cacheKey, final Object obj) {
        // TODO - Hook in your cache of choice, could be a SimpleTimeoutCache
    }

 

}

3 Comments Permalink

Recently, I needed to reproduce a problem that was only occurring when the storefront was under heavy load. As a developer, I cannot easily generate realistic load in my environment and I don't have access to a live production system, so I used JMeter to help me out. JMeter is a Java application for performing load tests on web applications. In this post, I'll show how to use JMeter to quickly set up a simple performance test against the Elastic Path Commerce storefront.

 

I wanted to see how long it would take to perform a search in the storefront for Canon products. In this case, I wanted JMeter to fire the following query a few hundred times using one or more threads: http://demo.elasticpath.com:8080/storefront/search.ep?categoryId=&keyWords=canon&submit=search

 

First, I created a thread group under the test plan tree node. (Right-click the test plan and choose Add->Thread Group.) If you want to simulate more than one user hitting the same search at the same time, you can set the number of threads in the thread group configuration.

img1_1.png

 

Next, I added a loop controller to my thread group. (Right-click the thread group and click Add->Logic Controller->Loop Controller.) Here, I defined how many times the loop will run. In my case, I checked the Infinite checkbox.

img2.png

 

Then, I added an HTTP request to the loop controller. (Right-clicking the loop controller and click Add->Sampler->HTTP Request.) I set the Server Name, Server Port, URL Path, chose GET in the dropdown and set three parameters in the request URL.

img3_1.png

 

At this point, I started the test by clicking Run->Start. JMeter starts to execute the HTTP request I defined in the loop controller. Since I set the loop count to Forever, it will continue to execute until I stop it.

 

Now, it would be a good idea to attach some reports to debug the execution. I tried a few reports and found that the View Results Tree presented all the information I needed. It shows the raw request sent by JMeter and the response data the server is providing. You can examine the server response to make sure the HTTP request you configured is doing what it is expected to do.

 

To add a View Results Tree report to your test plan, right-click your HTTP request test, and choose Add->Listener->View Results Tree.

You can finally see the results of each call on the report.

 

img4.png

 

You can build more complex tests, with several simultaneous requests over different URLs. For example, you can configure JMeter to first simulate a user logging in to your application (and hold session information), then a few requests to simulate the user navigating through the web application and then repeat this test a few hundred times using as many threads as you want (or your machine can handle).

 

Using JMeter, I was able to simulate a sufficently heavy load to reproduce the problem I wanted to reproduce and this post should help you do the same thing if you face a similar problem. For more details on JMeter, go to: http://jakarta.apache.org/jmeter/

 

Hope you enjoy!

2 Comments Permalink

Out of the box, Elastic Path is configured to serve pages for the storefront. Some customers, however, need more flexibility. For example, we have a customer with content in a Content Management System (CMS) that needed to include links to EP content. The approach taken was to make Elastic Path content available as widgets which could be loaded remotely using JavaScript. This approach has the limitation that if the end user has JavaScript turned off then they will not be able to access the shopping functionality. The jQuery JavaScript library was used.

 

Product Browsing

To enable product browsing, the ShoppingItemConfigController was customized to support two request parameters: type and  productWidgetId. When the type parameter was set to "widget", the  ShoppingItemConfigController returned the widget product view. This  product view was customised to fit in the small space allowed on the  page and a custom template was created for this. The productWidgetId parameter was used to identify the widget so that  multiple widgets could exist on one page.

 

The host page from the CMS included a div at the required location for the product widget:

<div class="blockElement" id="product-widget-1"></div>

 

A script element on the page included the following code:

$(document).ready(function() {
    $('#product-widget-1').load('storefront/category1/productCode.html?type=widget&productWidgetId=product-widget-1');
}

This code executes after the page loads but before all sub-content (e.g. images)  is loaded, sending an asynchronous request to EP for the product content and loading it in  the div with the id "product-widget-1".

 

Add to Cart

To add an item to the cart, an asynchronous call to DWR was made using the widget id to identify the SKU and quantity to add.

<form id="skuSelectForm" name="skuSelectForm" method="post" onsubmit="return addToCartSubmit('#$productWidgetId'); ">

 

function addToCartSubmit(widgetId) {      
    shoppingCartAjaxController.addSkuToCart(jQuery(widgetId + ' 
        .skuCodeParameter').val(), jQuery(widgetId +'-quantitySelect').val(), 
        function(data) {
            refreshShoppingCartSummary();
       
        });
   return false;
}


Shopping Cart Summary

When the addSkuToCart request returns, the refreshShoppingCartSummary() method is called. This is defined in a JavaScript file included in the hosted page. It makes another asynchronous call in order to determine the number of items in the cart. (Note: for greater network efficiency, the number of items in the cart could be returned from addSkuToCart.)

function refreshShoppingCartSummary() {
    shoppingCartAjaxController.getCartItemCount(function(data) { 
        jQuery('#cartSummaryText').text(data); 
    });            
};

 

Shopping Cart Summary Popup

The jQuery hover method was used  to detect when the mouse was over the icon and display a popup showing the contents of the cart. A DWR call was made to retrieve the cart data. To reduce the number of round trips, this call was designed to return all required information for the cart and its items. JavaScript was used to build the DOM tree for this data. (Note that, in production, a JS client side templating engine should be considered for this role.)

 

The next issue was making sure the popup remained open when the mouse moved to the checkout button. This was done using JavaScript timeouts; when the hover out function was called, the timeout would start. When a hover in function was called for the cart icon or for the popup, the timeout would be cancelled. If the timeout fired without being cancelled, the popup would be closed.

 

    $('#shoppingCartMenu').hover(function() {
        shoppingCartAjaxController.getCartSummary(function(cart) {
            var popupHtml = '<div class="cartTop">';           
            popupHtml += '</div>';
            popupHtml += '<div class="cartBottom transparent">';
            popupHtml +=    '<div class="yourCart">';
            popupHtml +=        '<p>YOUR CART</p>';
            popupHtml +=    '</div>'

            for (var i=0; i<cart.shoppingItems.length; i++) {
                popupHtml += '<div class="product">';
                shoppingItem = cart.shoppingItems[i];
                popupHtml +=    '<div class="items">';
                popupHtml +=        '<div class="itemsImg">';
                popupHtml +=            '<img width="35" height="41" border="0" alt="' + shoppingItem.localizedProductName;
                popupHtml += '" src="/storefront/renderImage.image?imageName=' + shoppingItem.imageFileName +'&width=35&height=41" id="productImage">';
                popupHtml +=        '</div>';
                popupHtml +=        '<div class="itemsText">';
                popupHtml +=            '<p><strong>' + shoppingItem.localizedProductName + '</strong></p>';
                popupHtml += '<p>';
                for (var j=0; j<shoppingItem.localizedSkuOptionDisplayNames.length; j++) {
                    if (j != 0) popupHtml += ',';
                    popupHtml += shoppingItem.localizedSkuOptionDisplayNames[j];
                }
                popupHtml += '</p>';
                popupHtml +=        '</div>';                                               
                popupHtml +=    '</div>';   
                popupHtml +=    '<div class="priceSubtotal">';
                popupHtml +=        '<p>Qty: ' + shoppingItem.quantity +'</p>';
                popupHtml +=    '</div>';
                popupHtml +=    '<div class="price">';
                popupHtml +=        '<h2><p>' + getCents(shoppingItem.total) + '</p><h2>' + getDollars(shoppingItem.total) + '</h2><p>$</p></h2>';
                popupHtml +=    '</div>';
                popupHtml += '</div>';
            }
            popupHtml += '<div class="subtotal">';
            popupHtml +=    '<div class="items">';
            popupHtml +=        '<p>ITEMS IN CART: ' + cart.numItems + '</p>';
            popupHtml +=    '</div>';   
            popupHtml +=    '<div class="priceSubtotal">';
            popupHtml +=        '<p>SUBTOTAL:</p>';
            popupHtml +=    '</div>';
            popupHtml +=    '<div class="price">';

            popupHtml +=        '<h2><p>' + getCents(cart.subTotal) + '</p><h2>' + getDollars(cart.subTotal) + '</h2><p>$</p></h2>';
            popupHtml +=    '</div>';                                               
            popupHtml += '</div>';                                   
            popupHtml += '<div class="checkout">';
            popupHtml +=    '<a href="/storefront/check-out.ep" title=""><img src="images/btn_checkout.gif" alt="" /></a>';
            popupHtml += '</div>';
            popupHtml += '</div>';
            $('#shoppingCartPopup').html(popupHtml);
            $('#shoppingCartPopup').css('display', 'block');
        });
    }, function() {
        shoppingCartPopupClose = setTimeout(closeShoppingCartPopup();, 500);
    });
   
    $('#shoppingCartPopup').hover(function() {
        clearTimeout(shoppingCartPopupClose);
    }, function() {
        shoppingCartPopupClose = setTimeout(closeShoppingCartPopup();, 500);
    })
1 Comments Permalink

A while back, I wrote about our decision to change to a two-page checkout process, with the main goal being to reduce checkout process abandonment. We piloted this checkout process on the Hockey Canada store and the results were extremely positive, but we weren't content to sit on our laurels. So, when we started re-designing the official Vancouver 2010 Olympic store, we challenged ourselves to take it to the next level -- and we cut the checkout process down to just one page.

 

Structurally, the new single-page checkout looks very much like the two-page checkout, with shipping information first, followed by billing and confirmation. The Elastic Path Commerce platform is flexible enough to handle multiple checkout process flows for the same store, so there was no significant Google Website Optimizer integration required to make this work.

 

Variant A (Control): Multi-page Checkout

 

Page 1 (sign in):

blog_old_1.png


 

Page 2 (shipping address):

blog_old_3.png


 

Page 3 (shipping method):

blog_old_4.png


Page 4 (billing & review):

blog_old_5.png


Page 5 (receipt)

blog_old_2.png


 

 

Variant B: Single Page Checkout

 

Page 1 (shipping, billing):

blog_new_1.png


Page 2 (receipt and optional user registration form):

blog_new_2.png

 

In A/B split testing, 50% of site traffic was redirected to the OOTB checkout, while the other 50% was served the new single-page checkout. By the time we reached 300 transactions, the winner was clear, and we stopped the experiment after 606 transactions. Google Website Optimizer concluded that the single-page checkout outperformed the out-of-the-box checkout by a whopping 21.8%. But what does that 21.8% really mean? GWO only counts goal conversions and does not link to any ecommerce data on Google Analytics, so we used Advanced Segments to get this data passed on to Google Analytics.

 

dfvnscm6_112d6j5vsdp_b.png

 

We defined two Advanced Segments by creating the following expressions:

 

  • Multi-step checkout: /(?:checkout|shipping-address|billing-address|delivery-options|billing-and-review)\.html.*
  • Single page checkout: /check-out\.html.*

 

This allowed us to track metrics like Average Order Value and Conversion Rate for each experiment variation.

 

Here's what we observed:

 

  • Successful completion rate for the entire checkout process increased by 257.26%.
  • Overall site conversion rate increased by 0.54%.
  • We also observed some unexpected improvements during this experiment, like an increase of 8.54% in the average order value!

 

While these numbers are impressive, they should not be used as the sole indicator of how single-page checkout performs. This is just what we observed when changing from the standard four-page checkout to a single-page checkout process on the Vancouver 2010 Olympic Store. Your mileage may vary, depending on your product, target market, etc. There's no silver bullet checkout process that works best for all business models. Doing your own A/B split testing will give you a better idea of what kinds of numbers you can expect.

1 Comments Permalink

If some of the recent performance-related posts by our much cherished Get Elastic blogger extraordinaire Linda are starting to worry you, fear not! With the newly released Elastic Path 6.2, the Product Development and Performance teams at Elastic Path have done a fantastic job ramping up the standard performance of the product. I'll let them brag about the numbers at a later date, but today's post is to dig into the guts of the caching introduced into the system, and how you can start tweaking some of the configuration settings to squeek out every millisecond for page responses.

 

Within Elastic Path, there are now two caches that are available to be tweaked:

  • Application-level: Sitting between the view layer and the data access layer
  • Persistence-level: A Level-2 cache, within the OpenJPA ORM framework

 

Application Level Cache

All products loaded within the Storefront application via the StoreProductService will be from an Ehcache-backed cache by default. Each application is responsible for loading products via a ProductRetrieveStrategy. You'll notice storefront has two new configurations to facilitate using Ehcache:

 

Cache.xml:
     <bean id="productCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
          <property name="timeToLive" value="600"/>
          <property name="timeToIdle" value="600"/>
     </bean>

     <bean id="cachingProductRetrieveStrategy" class="com.elasticpath.sfweb.service.impl.EhCacheProductRetrieveStrategyImpl">
          <property name="productService" ref="productService" />
          <property name="cache" ref="productCache" />
     </bean>

 

ServiceSF.xml:
    <alias name="cachedSettingsReader" alias="settingsReader"/>
    <alias name="cachingProductRetrieveStrategy" alias="productRetrieveStrategy"/>

 

You'll note that the storefront is now using aliases in the Spring configuration to override the same bean definitions in the default core service.xml. This allows the storefront to setup caching-specific classes. For tweaking purposes, the productCache bean definition should be updated to optimize the Ehcache settings. By default, Spring's EhCacheFactoryBean will initialize the cache to allow overflow to disk, use LRU eviction and limit the in-memory size to 10k objects. For catalogs with a larger product mix, these settings should be optimized and potentially moved to an distributed cache via Terracotta to keep the JVM heap size to a reasonable level.

 

You can choose to add new properties here to tweak these values, or add an ehcache.xml configuration file to the classpath and define a specific cache name as part of the productCache definition to use. A quick tip on monitoring the cache statistics for tweaking the settings during load test is to setup JMX monitoring for the storefront cache. This can be done via two steps:

 

Running the appserver with remote JMX enabled (no authentication for a non-production environment):

-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=6969 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

 

Configuring the JMX beans for Ehcache in cache.xml. Note in this case we are explicitly setting up a cache manager, which we can also use to specify a custom Ehcache configuration file instead of the default ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd ">

    <bean id="productCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
        <property name="timeToLive" value="600"/>
        <property name="timeToIdle" value="600"/>
        <property name="cacheManager" ref="cacheManager" />
    </bean>

    <bean id="cachingProductRetrieveStrategy" class="com.elasticpath.sfweb.service.impl.EhCacheProductRetrieveStrategyImpl">
        <property name="productService" ref="productService" />
        <property name="cache" ref="productCache" />
    </bean>

    <bean id="cacheManager"
        class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    </bean>

    <!-- Spring initialization of ehCache's mbeans -->
    <bean id="ehCacheMBeanRegistration"
        class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="staticMethod"
            value="net.sf.ehcache.management.ManagementService.registerMBeans" />
        <property name="arguments">
            <list>
                <ref bean="cacheManager" />
                <ref bean="mbeanServer" />
                <value>true</value>
                <value>true</value>
                <value>true</value>
                <value>true</value>
            </list>
        </property>
    </bean>

    <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
        <property name="locateExistingServerIfPossible" value="true" />
    </bean>

</beans>

 

Connecting up with jConsole lets us check the cache settings are properly configured, and to check the statistics, as per the below image. We should see updates to statistics as you browse the storefront and load more products:

screen-capture-1.png

 

 

Persistence Level Cache

As part of the Elastic Path 6.2 release, the included OpenJPA library has been upgraded to a 1.2.1 version, which overcomes some data cache issues in the previous 1.0.1 version. Please see the OpenJPA 1.2.1 documentation for all details on the native OpenJPA datacache. We'll go over some of the changes that enable the data cache using the native implementation.

 

Annotations:

All transactional persistent entities (anything submitted or updated regularly as an online transaction such as orders, payments and customers) have a new data cache annotation so that they are non-cacheable. By default, entities are enabled in the data cache unless this annotation is present and explicitly disabled the caching. All relatively static data, such as catalog entities should be part of the cache and thus will be missing this new annotation.

 

@DataCache(enabled = false)

 

Persistence.xml Configuration:

As part of the persistence unit configuration, three new properties are configured by default:

            <property name="openjpa.DataCache" value="true"/>
            <property name="openjpa.RemoteCommitProvider" value="sjvm"/>
            <property name="openjpa.DataCacheTimeout" value="1000"/>

 

These values are used for:

  • openjpa.DataCache - enabling the cache, and specifying the cache properties. We recommend tweaking this value to accomodate the cache size according to the size of the underlying data: (true(CacheSize=25000, SoftReferenceSize=0))
  • openjpa.RemoteCommitProvider - specifying the commit provider. For a clustered setup, evictions should be handled via configuring this setting to use JMS or TCP-based evictions.
  • openjpa.DataCacheTimeout - maximum time to live for entities in the cache

 

Also suggested is to tune the query cache, which is enabled with default values when the DataCache property is set to true:

<property name="openjpa.DataCache" value="true(CacheSize=25000, SoftReferenceSize=0)"/>
<property name="openjpa.RemoteCommitProvider" value="sjvm"/>
<property name="openjpa.QueryCache" value="CacheSize=25000, SoftReferenceSize=0"/>

 

Tip: Catching Cache Hits and Misses

If caching is enabled and you're still seeing a large amount of database queries from the storefront server, you can enable data cache Log4j tracing and grep out the hits and misses logged to track down which entities and queries are mistakingly hitting the database. Most of the times, these can be tracked down to accidental evictions or entities within the inheritance hierachy being disabled from cache.

 

log4j.category.openjpa.DataCache=TRACE

 

This spits out some logs similar to below that's easily greppable to count and track which entities are creating problems:

DEBUG 2009-09-10 12:56:33,918 org.apache.renamed.openjpa.lib.log.CommonsLogFactory$LogAdapter.trace(CommonsLogFactory.java:76) - Cache hit while looking up key "com.elasticpath.domain.attribute.impl.ProductTypeProductAttributeImpl-1".
DEBUG 2009-09-10 12:56:33,926 org.apache.renamed.openjpa.lib.log.CommonsLogFactory$LogAdapter.trace(CommonsLogFactory.java:76) - Cache miss while looking up key "org.apache.renamed.openjpa.datacache.QueryKey@b7ee9cab[query:[SELECT ps.skuCodeInternal FROM ProductSkuImpl ps

 

OpenJPA Cache Plugins

The native data cache within OpenJPA is architected to be swappable via plugin configuration. This allows the ability to swap in alternative caching technologies like Ehcache or Oracle Coherence to support extensive scalability requirements, as both Ehcache/Terracotta and Oracle Coherence support distributed caching setups. We've tested Ehcache and Coherence plugins internally with favourable results.

 

An Ehcache plugin is provided by the Ehcache group, however this version must be repackaged to match the custom package names of the Elastic Path specific org.apache.renamed.openjpa jar. Once a repackaged instance of the Ehcache-OpenJPA jar is in the classpath, the configuration changes to:

<property name="openjpa.DataCacheManager" value="ehcache"/>

 

Similarily enough, OpenJPA commiter Pinaki has posted the initial workings of an Oracle Coherence plugin on his blog, along with some additional JPA caching insights. Elastic Path with OpenJPA and Oracle Coherence is probably worth an entire blog entry in itself (coming soon!), but the same configuration settings apply, along with the need to repackage the code to point at the Elastic Path OpenJPA jar:

 

<property name="openjpa.DataCacheManager" value="coherence"/>

 

So there it is, two new caching mechanisms to tweak and fine-tune as part of load tests that should favourably reflect storefront page load times, and hopefully conversion rates and green dollar signs. Feel free to ask away about some of our load testing and tuning war stories. We'd be happy to talk about our hands on experience with the recent caching work.

6 Comments Permalink

Checkout Revisited

Posted by Janis Lanka Oct 1, 2009

The checkout process is quite possibly the scariest part of the online shopping experience. Customers get anxious about divulging sensitive personal information, not to mention parting with their hard-earned cash. Long and complicated customer registration forms to fill out, strange error messages, security concerns, too many steps, etc., all contribute to shopper frustration and all too often, cart abandonment.

 

We recently did a review of our current out-of-the-box checkout process and we made the following observations:

 

  • There are too many steps, resulting in a high cart abandonment rate.
  • The distinction between customer types is confusing. "Am I a registered customer or an existing one?" "What's a 'guest'?"
  • Giving the option to register during the first step of checkout is an unnecessary distraction. It gives customers too much time to change their mind!
  • While the cart total is visible during each step, it doesn't provide a breakdown on specific items that have been added to the cart.
  • Customers who have a promotion code don't know where to enter it, which leads to frustrated calls to customer service.

 

We started looking at ways to simplify the process and we came up with a shorter, smoother, and clearer checkout process that works well for various industries and customer types/profiles. In this new and improved checkout process, there are only three stages:

  • shipping information
  • billing information
  • receipt and (optional) registration.

Shipping Information

In the new process, the login and shipping information pages are merged into a single page. If a returning customer can't remember their login name or password, they can skip it and just enter the shipping details without any extra clicks. Here, guests and new customers are treated the same.


The contents of the shopping cart are displayed on this page and on both of the other checkout pages, so the customer always knows exactly what they're buying. The coupon and promo code fields are also available at each step, under the shopping cart summary. Once a coupon or promo code is applied, the cart total is updated automatically via AJAX.

c_shipping.png

 

Billing Information

The billing step asks for the customer's email address and any other information required for invoice purposes. All the shipping information entered in the last step appears once again for final customer review. The final confirmation block is a good place to have a subscription or "sign up for email alerts" check box. Even if customers don't create an account, they may still choose to be on the mailing list.

c_billing.png

 

Receipt and Registration

Unlike the standard checkout process, the customer is only asked to register for an account after they've made their purchase. Some retailers might worry that nobody will register unless they're required to. If that's a concern, you can provide some incentive for registering (like a discount on their next purchase). Keep in mind that the point is to avoid distracting the shopper from their goal, which is to make their purchase.

 

If the customer registers for an account after checkout is complete, that order will be associated with the new account. Unfortunately, previous orders they created will not be, even if they were created under the same email address as the new account. (This is for security reasons.)

c_confirmation.png

 

This new checkout process will require using AJAX/JavaScript validation at every step, making it a bit tedious to code, but worth it in the long run.

 

We are currently in the process of integrating this new process for several of our clients and will be keeping a close eye on their performance and results. Of course, this process may not work for every ecommerce site. Hopefully this will at least give you some inspiration for ways to improve your own checkout process. There are always areas that can be simplified and streamlined. Just keep testing various aspects of your checkout process. That's the only way to really learn what works best for your audience.

 

Stay tuned and subscribe to our RSS feed because down the road we will follow up with another post regards the results and observations.

1 Comments Permalink

If you've been reading the blog posts by the Elastic Path QA team, you already know that we use Selenium-RC to automate storefront testing and Squish for automated Commerce Manager client testing. Selenium Remote Control (Selenium-RC) is an amazing testing tool, but it only works for web applications running in the browser. Squish is also a great tool, but it only works with desktop applications, like the CM client. For some of our test cases, we need to make changes in the CM client and then verify the changes in the storefront web application. In the past, we had to test this manually, which was time-consuming.

 

Now, we've come up with a way to fully automate this and generate a complete report with the test results. Basically, the strategy is to use a Squish script to invoke Selenium-RC, which then runs our Selenium scripts. This has saved us a lot of time. I'll explain how it works.

 

If you want to try this yourself, you need the following software on your test machine:

  • Java 1.4 or later
  • Ant 1.7.0 or later
  • Firefox 2.0 or later
  • Selenium IDE (for creating test suites and test cases)
  • Selenium RC (for running the tests)
  • Squish for Java

 

Selenium IDE and Selenium server can both be downloaded from the Selenium download page.

 

Also, make sure that Firefox is not remembering passwords; in Firefox,select Tools -> Options -> Security and uncheck the Remember password for sites option.

And if you are using self-signed certificate, a dialog appears when you open EP's account page to confirm that you want to access it. This will break the automated test script, so you need to go to the My Account page in Firefox, accept the certificate, and then close the browser. Next time the page opens (when you run the automated test), the dialog will not appear.

 

Creating your Selenium test project

  1. Use Selenium IDE to create a Selenium suite and test cases.
  2. Create a folder named epautotest. Inside this folder, create a lib folder and a tests folder.
  3. Copy build.xml to the epautotest folder. The build.xml file is the Ant build script, which includes all information Ant needs to run our Selenium tests. You can edit this file to suit your environment. For example, set the testDomain property to the URL of your storefront.

    build xml2.jpg

  4. Copy user-extension.js, summary.xsl, Tidy.jar and selenium-server.jar to  the lib folder.
    • user-extension.js contains functions that extend the base Selenium functionality.
    • summary.xsl is the test report template file.
    • Tidy.jar is the open source library we use to generate the test report.
    • selenium-server.jar is the library required to execute the Selenium tests. (You can get it from your Selenium RC installation.)
    • Copy the Selenium test suite and test cases to the tests folder.
    • Copy your Firefox profile folder (found in C:\Documents and Settings\<your_username>\Application Data\Mozilla\Firefox\Profiles) to the epautotest folder.
    • Modify the build.xml file to point to the copy of your Firefox profile folder.

     

    Running the Selenium tests

    Now, you want to make sure that your Selenium tests run. To run the tests, simply type ant in the epautotest folder. When the tests are finished, a summary can be found in the file test-results\test-summary.html. The folders under test-results mirror the folders under the tests folder and each individual test suite's report can be found under it's relevent folder. Note that the previous test run's results can be found in the test-results.old folder.

    Using Squish to run Selenium tests

    Next, you need to create a batch file that can be invoked by your Squish scripts. This will run the Ant build script that runs your Selenium tests.

    1. Create a batch file in the Squish scripts folder named ep.bat. The contents should look something like this: ant -f c:\epautotest\build.xml
    2. In your Squish scripts, when you want to run the Selenium tests, add a command to invoke the batch file. This command might look like this:

    var i = OS.system("c:\\epautotest\\ep.bat");

     

    We can get the test reports in the c:\epautotest\test-results folder.

    With this fairly simple integration between Selenium and Squish, we've been able to extend automated test coverage and save valuable QA time. If you'd like to see how this works, give it a try. You can use the files I've attached to this post. Let me know if you have questions!

    0 Comments Permalink

          

    Introduction

    Asset management has changed significantly in EP 6.1 to make deployment of multiple stores easier. With EP 6.1, multiple stores may be run in a single WAR file, hence there is no need to copy WAR instances to deploy a new store. Asset types, which include images, CSS, JavaScript, email templates, storefront velocity templates, VM_global_libraries and Message Resource property files, have been moved out of the WAR deployment location and into a new directory elsewhere on the file system. The previous structure of the assets directory has also changed.

     

    The assets root directory structure is as follows:

     

    - assets

    + cmassets

    + digitalassets

    + images

    + import

    + storeassets

    + themes

     

    The absolute path for assets is specified in your deployment's COMMERCE/SYSTEM/ASSETS/assetLocation setting. EP6.1 also introduces the "Settings Framework," a new component that contains system and store configuration settings that used to be captured in commerce-config.xml and other files. The COMMERCE/SYSTEM/ASSETS/assetLocation setting value must be configured before you start your storefront application to point at your assets root directory, for example c:\assets or /opt/assets.

     

    • The cmassets directory is also new in EP 6.1 and contains the Commerce Manager specific files, such as user account email templates and the import report email template.
    • The digitalassets directory has been around for several releases and still contains the downloadable digital product files.
    • The images directory is also not new and contains your product display images.
    • The import directory was introduced in EP 6.0 and contains all import CSV files used by the Commerce Manager. Please note that these files are not used by the Import/Export tool, which is a separate component.
    • The storeassets directory is new in EP 6.1 and only contains the PowerReviews required by each store. This directory is specific to each store, but does not form part of a theme.
    • The themes directory is new and serves as the root directory for all themes used by your stores. Themes are used to define the look and feel of stores in EP. You can create multiple themes and associate these with one or more stores. You can also customize theme elements (images, CSS, JavaScript) for individual stores without affecting other stores that use the same theme.

     

     

    A theme consists of a set of Velocity templates and resources required to display a storefront (stylesheets, images, properties files, etc.). Each theme is stored in its own directory under the assets/themes directory:

     

    - assets

    + cmassets

    + digitalassets

    + images

    + import

    + storeassets

    - themes

      + electronics

      + mytheme1

      + mytheme2

      + ...

     

    Each theme must contain a default directory. This directory contains the templates and the template-resources directories. The templates directory contains the Velocity templates used by the Velocity Engine to render HTML pages. The template-resources directory contains the stylesheets, Javascript and image files referred to by the rendered HTML pages.

     

    - assets

    + cmassets

    + digitalassets

    + images

    + import

    + storeassets

    - themes

      - electronics

        - default

          - template-resources

            + images

            + js

            + stylesheet

          - templates

            - velocity

              + index.vm

    Within these directories, you can use any directory structure you want to organize files, but keep in mind the file-based fallback mechanism used within themes.

     

     

    Themes can be customized to suit the needs of individual stores. To customize a theme for a specific store, create a directory under the theme and set the directory name to the store code. For example, to customize mytheme1 for the SNAPITUP store, create a directory named SNAPITUP under mytheme1. The directory structure should look similar to the following:

     

    - assets

    + themes

    - mytheme1

      + default

      + SNAPITUP

     

    In the store-specific directory, only create copies of the files that you want to customize. The copies must exist in the same relative directory structure within the store-specific directory. For example, to customize the mytheme1/default/template-resources/images/image-not-available.png image for the SNAPITUP store, place the custom version of image-not-available.png in mytheme1/SNAPITUP/template-resources/images.

     

    - assets

    - themes

      - mytheme1

        - default

          - template-resources

            - images

              + image-not-available.png (default image)

              + ... all other shared images

        - SNAPITUP

          - template-resources

            - images

              + image-not-available.png (store overriding image)

     

    It is not necessary to copy the entire contents of the default directory into the store-specific directory. You only need to include copies of the files that you are customizing. The application looks for files in the store-specific directory first; if it does not find a file in the store-specific directory, it then looks for it in the default directory. In other words, your customized copy of the file or image acts as an override for the default file or image. This file-based overriding mechanism is used for images, stylesheet, JavaScript and Velocity template files.  Access to these resources is handled by resource controllers, which in turn use resource retrieval strategies for greater flexibility.

     

    WEB-INF/conf/spring/web/url-mapping.xml

    <bean id="SimpleUrlHandlerMapping"

        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

      <property name="mappings">

          <props>

          <prop key="/images/**/*">storeThemeAssetResourceController</prop>

          <prop key="/stylesheet/**/*">storeThemeAssetResourceController</prop>

          <prop key="/js/**/*">storeThemeAssetResourceController</prop>

          </props>

      </property>

    </bean>

    <!-- Resource controllers for resource retrieval -->

    <bean id="storeThemeAssetResourceController"

        class="com.elasticpath.sfweb.controller.impl.AssetResourceControllerImpl"

        parent="abstractEpController">

      <property name="resourceRetrievalStrategy"

                ref="storeThemeAssetRetrievalStrategy"/>

    </bean>

    Message Sources use a property based overriding mechanism, as opposed to the file based overriding for other types of assets like images. Message Sources are a collection of property files, which associate message keys to message text in different languages. Take, for example, the globals.firstname property key that's located in 2 files, one referencing the English message and the other the French message.

    - assets

    - themes

      - mytheme1

        - default

          - templates

            + globals.properties  : globals.firstname=First Name

            + globals_fr.properties: globals.firstname=Prenom

     

    To override the French globals.firstname message for the SNAPITUPUK store, one need only override one property, not the entire property file. In this case the global_fr.properties file need only contain a single property entry for globals.firstname.

     

    - assets

    - themes

      - mytheme1

        - default

        - templates

            + globals_fr.properties: globals.firstname=Prenom (default)

        - SNAPITUP

          - templates

            + globals_fr.properties: globals.firstname=Prénom (overriding)

     

    In fact, the file name need not even start with "globals" as all properties are loaded into a collection at which point file names are no longer relevant. The only requirement is that it contain the language code. For example, having the globals.firstname property override in a file named no-name-in-particular_fr.properties, will have the same effect. If no language code is present, English is assumed.  For reference, see the Message Source configuration in the Storefront and Commerce Manager web applications.

     

    WEB-INF/conf/spring/views/velocity/velocity.xml

    <bean id="messageSource"

        class="com.elasticpath.commons.util.impl.StoreThemeMessageSource" >

      <property name="messageSourceCache">

          <ref bean="messageSourceCache"/>

      </property>

      <property name="storeConfig">

          <ref bean="threadLocalStorage"/>

      </property>

      <property name="globalMessageSource">

          <ref bean="globalMessageSource"/>

      </property>

      <!-- True if you want missing messages to show up

            as just the key used to look for them. e.g. user.firstname.

            False will allow exception to be thrown, and stop template render.

      -->

      <property name="useCodeAsDefaultMessage">

          <value>true</value>

      </property>

    </bean>

     


    Using Store Editor

     

    You can select the theme using the new store editor:

    1.  In Commerce Manager, choose *Activity*->*Configuration*.

    2.  Under *Stores*, click *Stores*.

    3.  Select the store, then click *Edit Store*.

    4.  Click the *Theme* tab.

    5.  In the _Theme_ box, enter the name of the theme folder. For example, if the theme folder is C:/Program Files/Apache Software Foundation/apache-tomcat-5.5.26/assets/themes/mytheme1, enter mytheme1.

    6.  In the Commerce Manager toolbar, click *Save*.


    Using Advanced Setting Configuration

     

    By default, the demo store is mapped to the electronics theme. To change the theme of a store:

     

    1.  In Commerce Manager, choose Activity -> Configuration.

    2.  Under System Administration, click System Configuration.

    3.  In the Property Name list, select COMMERCE/STORE/theme.

    4.  Click New (next to the Defined Values list).

    5.  Set the values as follows:

    ·        Context: The store code. For example, for the SnapItUp demo store, the store code is SNAPITUP.

    ·        Value: The name of the theme folder. For example, if the theme folder is C:/Program Files/Apache Software Foundation/apache-tomcat-5.5.26/assets/themes/mytheme1, enter mytheme1.

    6.  Click Save.

     

     

    During the development of EP 6.1 it was clear that we had to find a way to make the Velocity engine's VM_global_library store specific since stores do not always share the same library macros.  In EP 6.0, Spring instantiated a VelocityEngine bean directly to inject into the onePageHelper bean, as OnePageHelperImpl requires a Velocity engine for some functionality. This bean is also injected into the velocityConfig bean which is what the VelocityView class uses to get the Velocity engine (it is effectively a factory). The VelocityView class retrieves the Velocity engine once, but we want it to get the appropriate store engine each time the view is asked to render a template.

     

    Among other smaller changes, the following classes needed to be added in EP 6.1 to correct the Velocity Engine behavior.

     

    <bean id="viewResolver"

    class="com.elasticpath.sfweb.view.velocity.StoreVelocityViewResolver">

      <property name="viewClass">

    <value>com.elasticpath.sfweb.view.velocity.StoreVelocityViewImpl</value>

      </property>

      <property name="velocityConfigurer" ref="velocityConfigurer"/>

      ...

    </bean>

    <bean id="velocityConfigurer"

        class="com.elasticpath.sfweb.view.velocity.StoreVelocityConfigurer">

      <property name="storeConfig" ref="threadLocalStorage"/>

      <property name="velocityPropertiesMap" ref="velocityPropertiesMap"/>

    </bean>

     

    StoreVelocityConfigurer

     

    This is a subclass of VelocityConfigurer which keeps a Map of velocity engines keyed by store code. This class just overrides the getVelocityEngine() method and uses an injected store config to retrieve the appropriate VelocityEngine instance from the map. It will call createVelocityEngine() if there is no value for the store currently in the map.

     

    StoreVelocityViewResolver

     

    This is a subclass of VelocityViewResolver which is store aware. It has setters for a StoreConfig and VelocityConfigurer which will typically be injected by Spring. This class overrides the buildView method and passes the store config and velocity configurer through to the StoreVelocityViewImpl class so it can render the store-specific view.  The old "global" WEB-INF/VM_global_library.vm library has been removed. You now have a VM_global_library in the templates/velocity directory of a theme or store-specific theme override. This will fall back exactly like the other assets.

     

    - assets

    - themes

      - mytheme1

        - default

          - templates

            - velocity

              + VM_global_library.vm (default fall-back)

        - SNAPITUP

          - templates

            - velocity

                + VM_global_library.vm (store specific override)

     

    Store Specific Report Templates

     

    Report Design files (*.rptdesign) are typically bundled as part of the Commerce Manager plugin. Packing Slip reports are store specific though, and had to be moved out of the plugin and uses the theme to take advantage of the file-based overriding mechanism.

     

    - assets

    - themes

      - mytheme1

        - default

          - templates

            - printartifacts

              + packing_slip.rptdesign (default report design)

        - SNAPITUP

          - templates

            - printartifacts

                + packing_slip.rptdesign (store specific override)

     

     

    The com.elasticpath.cmweb.controller.impl.CacheInvalidationControllerImpl defined in the CM Server is useful to invalidate caches of various types based on an invalidation strategy. The default configuration, as displayed below and duplicated in the Storefront, uses the TriggeredCacheInvalidationStrategy to invalidate the Velocity engines and message resources.

     

    com.elasticpath.cm/WEB-INF/conf/spring/web/url-mapping.xml:

    <prop key="/invalidate-cache.ep">cacheInvalidationController</prop>

    <bean id="cacheInvalidationController"

        class="com.elasticpath.cmweb.controller.impl.CacheInvalidationControllerImpl">

      <property name="cacheInvalidationStrategy"

                ref="triggeredCacheInvalidationStrategy" />

    </bean>

    com.elasticpath.cm/WEB-INF/conf/spring/service/serviceCM.xml:

    <bean id="triggeredCacheInvalidationStrategy"

    class="com.elasticpath.commons.util.impl.TriggeredCacheInvalidationStrategyImpl">

      <property name="invalidatableCaches">

          <bean class="java.util.HashSet">

              <constructor-arg>

                  <set>

                      <ref bean="velocityEngineFactory"/>

                      <ref bean="messageSourceCache"/>

                  </set>

              </constructor-arg>

          </bean>

      </property>

    </bean>

    Be sure to have the invalidation URL secured, as it will cause a hit in performance every time the velocity engine is recreated, which reloads all the message property files.

     

     

    For information on configuring PowerReviews in Elastic Path, see the PowerReviews Wiki page at https://wiki.elasticpath.com/display/EP61DEPLOY/5+-+PowerReviews+(optional) in the Deployment Guide. For information on the Power Reviews system, see the PowerReviews website at http://www.powerreviews.com.  The Power Reviews component is enabled and disabled by the boolean store-specific setting COMMERCE/STORE/POWERREVIEWS/powerreviewsEnabled. The value of the setting is passed to Velocity code by the powerReviewsEnabledHelper Spring bean defined in com.elasticpath.sf/WEB-INF/conf/spring/service/serviceSF.xml. Power Reviews are rendered by JavaScript code, and examples can be found in the guidedSkuTemplate.vm and productTemplate.vm templates.

     

    WEB-INF/conf/spring/web/url-mapping.xml

    <bean id="SimpleUrlHandlerMapping"

        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

      <property name="mappings">

          <props>

          <prop key="/power-reviews/pwr/engine/js/**/*">

                storeAssetResourceController</prop>

          <prop key="/power-reviews/pwr/engine/**/*">

                storeAssetResourceController</prop>

          <prop key="/power-reviews/pwr/content/**/*">

                storeAssetResourceController</prop>

          </props>

      </property>

    </bean>

    <!-- Resource controllers for resource retrieval -->

    <bean id="storeThemeAssetResourceController"

        class="com.elasticpath.sfweb.controller.impl.AssetResourceControllerImpl"

        parent="abstractEpController">

      <property name="resourceRetrievalStrategy"

                ref="storeThemeAssetRetrievalStrategy"/>

    </bean>

    <bean id="storeAssetResourceController"

        class="com.elasticpath.sfweb.controller.impl.AssetResourceControllerImpl"

        parent="abstractEpController">

      <property name="resourceRetrievalStrategy"

                ref="storeResourceRetrievalStrategy"/>

    </bean>

    0 Comments 0 References Permalink