From b92a0a593f75ce1c73fc4dadefef8ed95a5d9e6e Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Tue, 9 Oct 2018 16:20:14 -0700 Subject: [PATCH 1/7] feature :add tracking for "Checkout Started" event --- src/containers/cart/fragments.gql | 2 ++ src/lib/tracking/constants.js | 6 ++++++ src/lib/tracking/trackCheckout.js | 30 ++++++++++++++++++++++++++++++ src/pages/checkout.js | 22 +++++++++++++++++++--- 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 src/lib/tracking/trackCheckout.js diff --git a/src/containers/cart/fragments.gql b/src/containers/cart/fragments.gql index 40c9f881cd..fd6d49c258 100644 --- a/src/containers/cart/fragments.gql +++ b/src/containers/cart/fragments.gql @@ -69,9 +69,11 @@ fragment CartCommon on Cart { displayAmount } itemTotal { + amount displayAmount } taxTotal { + amount displayAmount } total { diff --git a/src/lib/tracking/constants.js b/src/lib/tracking/constants.js index 2e28dd82e0..ce10cbe2c4 100644 --- a/src/lib/tracking/constants.js +++ b/src/lib/tracking/constants.js @@ -1,5 +1,11 @@ export default { CART_VIEWED: "Cart Viewed", + CHECKOUT_STARTED: "Checkout Started", + CHECKOUT_STEP_VIEWED: "Checkout Step Viewed", + CHECKOUT_STEP_COMPLETED: "Checkout Step Completed", + ORDER_COMPLETED: "Order Completed", + ORDER_UPDATED: "Order Updated", + PAYMENT_ENTERED: "Payment Entered", PRODUCT_VIEWED: "Product Viewed", PRODUCT_ADDED: "Product Added", PRODUCT_REMOVED: "Product Removed" diff --git a/src/lib/tracking/trackCheckout.js b/src/lib/tracking/trackCheckout.js new file mode 100644 index 0000000000..7575dafb6c --- /dev/null +++ b/src/lib/tracking/trackCheckout.js @@ -0,0 +1,30 @@ +import track from "./track"; +import getCartItemTrackingData from "./utils/getCartItemTrackingData"; + +/** + * trackCheckout HOC tracks the "Checkout Started" event + * @name trackCheckout + * @param {Object} options options to supply to tracking HOC + * @returns {React.Component} - component + */ +export default (options) => + // eslint-disable-next-line no-unused-vars + track(({ router }, state, functionArgs) => { + const { cart, action } = (functionArgs && functionArgs[0]) || []; + const { checkout: { summary }, items, shop } = cart; + const products = []; + + items.forEach((item) => { + products.push(getCartItemTrackingData(item)); + }); + + return { + action, + value: summary.itemTotal.amount, + revenue: summary.itemTotal.amount, + shipping: summary.fulfillmentTotal, + tax: summary.taxTotal, + currency: shop.currency.code, + products + }; + }, options); diff --git a/src/pages/checkout.js b/src/pages/checkout.js index 7d6e0321c2..029ca96c75 100644 --- a/src/pages/checkout.js +++ b/src/pages/checkout.js @@ -18,6 +18,9 @@ import LockIcon from "mdi-material-ui/Lock"; import withCart from "containers/cart/withCart"; import Link from "components/Link"; import CheckoutSummary from "components/CheckoutSummary"; +import trackCheckout from "lib/tracking/trackCheckout"; +import PageLoading from "components/PageLoading"; +import TRACKING from "lib/tracking/constants"; const styles = (theme) => ({ checkoutActions: { @@ -164,6 +167,9 @@ class Checkout extends Component { this.handleRouteChange(); } + @trackCheckout() + trackAction() {} + /** * * @name handleRouteChange @@ -171,18 +177,22 @@ class Checkout extends Component { * @return {undefined} */ handleRouteChange = () => { - const { cart, router: { asPath } } = this.props; + const { cart } = this.props; // Skipping if the `cart` is not available if (!cart) return; - if (hasIdentityCheck(cart) && asPath === "/cart/login") { + if (hasIdentityCheck(cart) && this.pagePath === "/cart/login") { Router.replaceRoute("/cart/checkout", {}, { shallow: true }); - } else if (!hasIdentityCheck(cart) && asPath === "/cart/checkout") { + } else if (!hasIdentityCheck(cart) && this.asPath === "/cart/checkout") { Router.replaceRoute("/cart/login", {}, { shallow: true }); } }; handleCartEmptyClick = () => Router.pushRoute("/"); + get pagePath() { + return this.props.router.asPath; + } + /** * * @name hasIdentity @@ -305,6 +315,9 @@ class Checkout extends Component { const hasAccount = !!cart.account; const displayEmail = (hasAccount && Array.isArray(cart.account.emailRecords) && cart.account.emailRecords[0].address) || cart.email; + // Track start of checkout process + this.trackAction({ cart, action: TRACKING.CHECKOUT_STARTED }); + return (
@@ -339,6 +352,9 @@ class Checkout extends Component { } render() { + const { isLoading, cart } = this.props; + if (isLoading || !cart) return ; + return ( {this.renderCheckoutHead()} From 06e8f23fd29bb45fd4864a813a46cce537f03bf1 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Wed, 10 Oct 2018 21:27:43 -0700 Subject: [PATCH 2/7] feature: wip add tracking to checkout funnel --- .../CheckoutActions/CheckoutActions.js | 106 +++++++++++++++++- src/containers/cart/withCart.js | 12 +- src/lib/tracking/constants.js | 6 +- src/lib/tracking/trackCheckoutStep.js | 25 +++++ src/pages/checkout.js | 7 -- 5 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 src/lib/tracking/trackCheckoutStep.js diff --git a/src/components/CheckoutActions/CheckoutActions.js b/src/components/CheckoutActions/CheckoutActions.js index d665c8407a..b51c7194b9 100644 --- a/src/components/CheckoutActions/CheckoutActions.js +++ b/src/components/CheckoutActions/CheckoutActions.js @@ -11,14 +11,26 @@ import withPlaceStripeOrder from "containers/order/withPlaceStripeOrder"; import Dialog from "@material-ui/core/Dialog"; import PageLoading from "components/PageLoading"; import { Router } from "routes"; +import track from "lib/tracking/track"; +import TRACKING from "lib/tracking/constants"; +import trackCheckout from "lib/tracking/trackCheckout"; +import trackCheckoutStep from "lib/tracking/trackCheckoutStep"; import { adaptAddressToFormFields, isShippingAddressSet } from "lib/utils/cartUtils"; +const { + CHECKOUT_STARTED, + CHECKOUT_STEP_COMPLETED, + CHECKOUT_STEP_VIEWED, + PAYMENT_INFO_ENTERED +} = TRACKING; + @withCart @withPlaceStripeOrder @inject("authStore") +@track() @observer export default class CheckoutActions extends Component { static propTypes = { @@ -43,19 +55,69 @@ export default class CheckoutActions extends Component { isPlacingOrder: false } - setShippingAddress = (address) => { + componentDidMount() { + const { cart } = this.props; + // Track start of checkout process + this.trackCheckoutStarted({ cart, action: CHECKOUT_STARTED }); + + const { checkout: { fulfillmentGroups } } = this.props.cart; + const hasShippingAddress = isShippingAddressSet(fulfillmentGroups); + // Track the first step, "Enter a shipping address" when the page renders, + // as it will be expanded by default, only record this event when the + // shipping address has not yet been set. + if (!hasShippingAddress) { + this.trackAction(this.buildData({ action: CHECKOUT_STEP_VIEWED, step: 1 })); + } + } + + @trackCheckout() + trackCheckoutStarted() {} + + @trackCheckoutStep() + trackAction() {} + + buildData = (data) => { + const { step, shipping_method = null, payment_method = null, action } = data; + + return { + action, + payment_method, + shipping_method, + step + } + } + + getShippingMethod = () => { + const { checkout: { fulfillmentGroups } } = this.props.cart; + const shippingMethod = fulfillmentGroups[0].selectedFulfillmentOption.fulfillmentMethod.displayName; + + return shippingMethod; + + } + + setShippingAddress = async (address) => { const { checkoutMutations: { onSetShippingAddress } } = this.props; // Omit firstName, lastName props as they are not in AddressInput type // The address form and GraphQL endpoint need to be made consistent const { firstName, lastName, ...rest } = address; - return onSetShippingAddress({ + const { data, error } = await onSetShippingAddress({ fullName: `${address.firstName} ${address.lastName}`, ...rest }); + + + if (data && !error) { + // track successfully setting a shipping address + this.trackAction(this.buildData({ action: CHECKOUT_STEP_COMPLETED, step: 1 })); + + // The next step will automatically be expanded, so lets track that + this.trackAction(this.buildData({ action: CHECKOUT_STEP_VIEWED, step: 2 })); + + } } - setShippingMethod = (shippingMethod) => { + setShippingMethod = async (shippingMethod) => { const { checkoutMutations: { onSetFulfillmentOption } } = this.props; const { checkout: { fulfillmentGroups } } = this.props.cart; const fulfillmentOption = { @@ -63,14 +125,50 @@ export default class CheckoutActions extends Component { fulfillmentMethodId: shippingMethod.selectedFulfillmentOption.fulfillmentMethod._id }; - return onSetFulfillmentOption(fulfillmentOption); + const { data, error } = await onSetFulfillmentOption(fulfillmentOption); + if (data && !error) { + // track successfully setting a shipping method + this.trackAction({ + step: 2, + shipping_method: this.getShippingMethod(), + payment_method: null, + action: CHECKOUT_STEP_COMPLETED + }); + + // The next step will automatically be expanded, so lets track that + this.trackAction({ + step: 3, + shipping_method: this.getShippingMethod(), + payment_method: null, + action: CHECKOUT_STEP_VIEWED + }); + } + } setPaymentMethod = (stripeToken) => { const { cartStore } = this.props; + const { brand } = stripeToken.token.card; // Store stripe token in MobX store cartStore.setStripeToken(stripeToken); + + // Track successfully setting a payment method + this.trackAction({ + step: 3, + shipping_method: this.getShippingMethod(), + payment_method: brand, + action: PAYMENT_INFO_ENTERED + }); + + // The next step will automatically be expanded, so lets track that + this.trackAction({ + step: 4, + shipping_method: this.getShippingMethod(), + payment_method: brand, + action: CHECKOUT_STEP_VIEWED + }); + } buildOrder = async () => { diff --git a/src/containers/cart/withCart.js b/src/containers/cart/withCart.js index e2a7322dcb..b045b2f4fd 100644 --- a/src/containers/cart/withCart.js +++ b/src/containers/cart/withCart.js @@ -320,10 +320,10 @@ export default function withCart(Component) { * @param {Function} mutation An Apollo mutation function * @return {undefined} No return */ - handleSetFulfillmentOption = async ({ fulfillmentGroupId, fulfillmentMethodId }) => { + handleSetFulfillmentOption = ({ fulfillmentGroupId, fulfillmentMethodId }) => { const { client: apolloClient } = this.props; - await apolloClient.mutate({ + return apolloClient.mutate({ mutation: setFulfillmentOptionCartMutation, variables: { input: { @@ -345,7 +345,7 @@ export default function withCart(Component) { handleSetShippingAddress = async (address) => { const { client: apolloClient } = this.props; - const result = await apolloClient.mutate({ + const response = await apolloClient.mutate({ mutation: setShippingAddressCartMutation, variables: { input: { @@ -356,8 +356,10 @@ export default function withCart(Component) { }); // Update fulfillment options for current cart - const { data: { setShippingAddressOnCart: { cart } } } = result; - await this.handleUpdateFulfillmentOptionsForGroup(cart.checkout.fulfillmentGroups[0]._id); + const { data: { setShippingAddressOnCart: { cart } } } = response; + this.handleUpdateFulfillmentOptionsForGroup(cart.checkout.fulfillmentGroups[0]._id); + + return response; } render() { diff --git a/src/lib/tracking/constants.js b/src/lib/tracking/constants.js index ce10cbe2c4..7a5782064f 100644 --- a/src/lib/tracking/constants.js +++ b/src/lib/tracking/constants.js @@ -5,8 +5,8 @@ export default { CHECKOUT_STEP_COMPLETED: "Checkout Step Completed", ORDER_COMPLETED: "Order Completed", ORDER_UPDATED: "Order Updated", - PAYMENT_ENTERED: "Payment Entered", - PRODUCT_VIEWED: "Product Viewed", + PAYMENT_INFO_ENTERED: "Payment Info Entered", PRODUCT_ADDED: "Product Added", - PRODUCT_REMOVED: "Product Removed" + PRODUCT_REMOVED: "Product Removed", + PRODUCT_VIEWED: "Product Viewed" }; diff --git a/src/lib/tracking/trackCheckoutStep.js b/src/lib/tracking/trackCheckoutStep.js new file mode 100644 index 0000000000..0f1a3eef1c --- /dev/null +++ b/src/lib/tracking/trackCheckoutStep.js @@ -0,0 +1,25 @@ +import track from "./track"; + +/** + * trackCheckoutStep HOC tracks the "Checkout Step Viewed | Completed" events + * @name trackCheckoutStep + * @param {Object} options options to supply to tracking HOC + * @returns {React.Component} - component + */ +export default (options) => + // eslint-disable-next-line no-unused-vars + track(({ router }, state, functionArgs) => { + const { + step, + shipping_method, + payment_method, + action + } = (functionArgs && functionArgs[0]) || []; + + return { + action, + step, + shipping_method, + payment_method + }; + }, options); diff --git a/src/pages/checkout.js b/src/pages/checkout.js index 029ca96c75..1ad4bda9df 100644 --- a/src/pages/checkout.js +++ b/src/pages/checkout.js @@ -18,9 +18,7 @@ import LockIcon from "mdi-material-ui/Lock"; import withCart from "containers/cart/withCart"; import Link from "components/Link"; import CheckoutSummary from "components/CheckoutSummary"; -import trackCheckout from "lib/tracking/trackCheckout"; import PageLoading from "components/PageLoading"; -import TRACKING from "lib/tracking/constants"; const styles = (theme) => ({ checkoutActions: { @@ -167,9 +165,6 @@ class Checkout extends Component { this.handleRouteChange(); } - @trackCheckout() - trackAction() {} - /** * * @name handleRouteChange @@ -315,8 +310,6 @@ class Checkout extends Component { const hasAccount = !!cart.account; const displayEmail = (hasAccount && Array.isArray(cart.account.emailRecords) && cart.account.emailRecords[0].address) || cart.email; - // Track start of checkout process - this.trackAction({ cart, action: TRACKING.CHECKOUT_STARTED }); return (
From 5a307d98812c380a97a27b9741c4e8229825916e Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Thu, 11 Oct 2018 12:30:49 -0700 Subject: [PATCH 3/7] fix: update snyk ignore --- .snyk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.snyk b/.snyk index d72b2da740..559806e477 100644 --- a/.snyk +++ b/.snyk @@ -4,6 +4,6 @@ version: v1.12.0 ignore: 'npm:chownr:20180731': - '*': - reason: Waiting on next-routes(https://github.com/fridays/next-routes) to fix this vulnerability + reason: This vulnerability only affects packages used in development, such as webpack, more details: https://github.com/isaacs/chownr/issues/14#issuecomment-421662375 expires: 2018-12-30T00:00:00.000Z patch: {} From 92e827e2bb180b9674edc24e49294e74b28c38fd Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Thu, 11 Oct 2018 12:31:05 -0700 Subject: [PATCH 4/7] chore: upgrade NextJS --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f4fd010287..8e9f625bea 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "mdi-material-ui": "^5.4.0", "mobx": "^4.1.1", "mobx-react": "^5.0.0", - "next": "^7.0.1", + "next": "^7.0.2", "next-routes": "^1.4.2", "passport": "^0.4.0", "passport-oauth2": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 7abd27a362..b5d53e209d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6049,9 +6049,9 @@ next-routes@^1.4.2: dependencies: path-to-regexp "^2.1.0" -next@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/next/-/next-7.0.1.tgz#58630126325dbeccf1926f32a8d9bd06ee43546c" +next@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/next/-/next-7.0.2.tgz#5ff6b3f0e6cf03ce539d5779a55dc1f8fb1759d7" dependencies: "@babel/core" "7.0.0" "@babel/plugin-proposal-class-properties" "7.0.0" From 8c08ac146624ce9b0af288f0022daa023b42f04c Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Thu, 11 Oct 2018 16:01:34 -0700 Subject: [PATCH 5/7] feature: add tracking for checkout step #4 --- .../CheckoutActions/CheckoutActions.js | 45 +++++++++++-------- src/lib/tracking/trackCheckoutStep.js | 17 +++---- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/components/CheckoutActions/CheckoutActions.js b/src/components/CheckoutActions/CheckoutActions.js index b51c7194b9..d959f0d95e 100644 --- a/src/components/CheckoutActions/CheckoutActions.js +++ b/src/components/CheckoutActions/CheckoutActions.js @@ -20,7 +20,7 @@ import { isShippingAddressSet } from "lib/utils/cartUtils"; -const { +const { CHECKOUT_STARTED, CHECKOUT_STEP_COMPLETED, CHECKOUT_STEP_VIEWED, @@ -59,7 +59,7 @@ export default class CheckoutActions extends Component { const { cart } = this.props; // Track start of checkout process this.trackCheckoutStarted({ cart, action: CHECKOUT_STARTED }); - + const { checkout: { fulfillmentGroups } } = this.props.cart; const hasShippingAddress = isShippingAddressSet(fulfillmentGroups); // Track the first step, "Enter a shipping address" when the page renders, @@ -77,22 +77,26 @@ export default class CheckoutActions extends Component { trackAction() {} buildData = (data) => { - const { step, shipping_method = null, payment_method = null, action } = data; + const { step, shipping_method = null, payment_method = null, action } = data; // eslint-disable-line camelcase return { action, - payment_method, - shipping_method, + payment_method, // eslint-disable-line camelcase + shipping_method, // eslint-disable-line camelcase step - } + }; } - getShippingMethod = () => { + get shippingMethod() { const { checkout: { fulfillmentGroups } } = this.props.cart; const shippingMethod = fulfillmentGroups[0].selectedFulfillmentOption.fulfillmentMethod.displayName; return shippingMethod; + } + get paymentMethod() { + const { stripeToken: { token: { card } } } = this.props.cartStore; + return card.brand; } setShippingAddress = async (address) => { @@ -113,7 +117,6 @@ export default class CheckoutActions extends Component { // The next step will automatically be expanded, so lets track that this.trackAction(this.buildData({ action: CHECKOUT_STEP_VIEWED, step: 2 })); - } } @@ -130,25 +133,23 @@ export default class CheckoutActions extends Component { // track successfully setting a shipping method this.trackAction({ step: 2, - shipping_method: this.getShippingMethod(), - payment_method: null, + shipping_method: this.shippingMethod, // eslint-disable-line camelcase + payment_method: null, // eslint-disable-line camelcase action: CHECKOUT_STEP_COMPLETED }); // The next step will automatically be expanded, so lets track that this.trackAction({ step: 3, - shipping_method: this.getShippingMethod(), - payment_method: null, + shipping_method: this.shippingMethod, // eslint-disable-line camelcase + payment_method: null, // eslint-disable-line camelcase action: CHECKOUT_STEP_VIEWED }); } - } setPaymentMethod = (stripeToken) => { const { cartStore } = this.props; - const { brand } = stripeToken.token.card; // Store stripe token in MobX store cartStore.setStripeToken(stripeToken); @@ -156,19 +157,18 @@ export default class CheckoutActions extends Component { // Track successfully setting a payment method this.trackAction({ step: 3, - shipping_method: this.getShippingMethod(), - payment_method: brand, + shipping_method: this.shippingMethod, // eslint-disable-line camelcase + payment_method: this.paymentMethod, // eslint-disable-line camelcase action: PAYMENT_INFO_ENTERED }); // The next step will automatically be expanded, so lets track that this.trackAction({ step: 4, - shipping_method: this.getShippingMethod(), - payment_method: brand, + shipping_method: this.shippingMethod, // eslint-disable-line camelcase + payment_method: this.paymentMethod, // eslint-disable-line camelcase action: CHECKOUT_STEP_VIEWED }); - } buildOrder = async () => { @@ -215,6 +215,13 @@ export default class CheckoutActions extends Component { if (data && !error) { const { placeOrderWithStripeCardPayment: { orders, token } } = data; + this.trackAction({ + step: 4, + shipping_method: this.shippingMethod, // eslint-disable-line camelcase + payment_method: this.paymentMethod, // eslint-disable-line camelcase + action: CHECKOUT_STEP_COMPLETED + }); + // Clear anonymous cart if (!authStore.isAuthenticated) { cartStore.clearAnonymousCartCredentials(); diff --git a/src/lib/tracking/trackCheckoutStep.js b/src/lib/tracking/trackCheckoutStep.js index 0f1a3eef1c..23298b9264 100644 --- a/src/lib/tracking/trackCheckoutStep.js +++ b/src/lib/tracking/trackCheckoutStep.js @@ -9,17 +9,18 @@ import track from "./track"; export default (options) => // eslint-disable-next-line no-unused-vars track(({ router }, state, functionArgs) => { - const { - step, - shipping_method, - payment_method, - action + const { + action, + payment_method, // eslint-disable-line camelcase + shipping_method, // eslint-disable-line camelcase + step } = (functionArgs && functionArgs[0]) || []; return { action, - step, - shipping_method, - payment_method + payment_method, // eslint-disable-line camelcase + shipping_method, // eslint-disable-line camelcase + step + }; }, options); From 51f381b4e98564fd045adb72f34aa4d41c21c5c5 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Thu, 11 Oct 2018 16:09:35 -0700 Subject: [PATCH 6/7] fix: snyk comment, does not allow semicolon --- .snyk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.snyk b/.snyk index 559806e477..0f011d33fc 100644 --- a/.snyk +++ b/.snyk @@ -4,6 +4,6 @@ version: v1.12.0 ignore: 'npm:chownr:20180731': - '*': - reason: This vulnerability only affects packages used in development, such as webpack, more details: https://github.com/isaacs/chownr/issues/14#issuecomment-421662375 + reason: This vulnerability only affects packages used in development, such as webpack, more details at https://github.com/isaacs/chownr/issues/14#issuecomment-421662375 expires: 2018-12-30T00:00:00.000Z patch: {} From 6ac8a86a58a5b0440d40305de229e4bba66eff6f Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Fri, 12 Oct 2018 17:13:19 -0700 Subject: [PATCH 7/7] fix: lint error --- src/components/CheckoutActions/CheckoutActions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/CheckoutActions/CheckoutActions.js b/src/components/CheckoutActions/CheckoutActions.js index cd2ca26004..d959f0d95e 100644 --- a/src/components/CheckoutActions/CheckoutActions.js +++ b/src/components/CheckoutActions/CheckoutActions.js @@ -118,7 +118,6 @@ export default class CheckoutActions extends Component { // The next step will automatically be expanded, so lets track that this.trackAction(this.buildData({ action: CHECKOUT_STEP_VIEWED, step: 2 })); } - } setShippingMethod = async (shippingMethod) => {