Add Tracks events to variations tab (#37607)

* Refactor `product-tracking` file

* Add variations tracks events

* Add changelog

* Refactor method `addProductAttributesTracks`

* Add `remove_variation` Tracks event

* Modify code comments

* Rename `query` prop

* Modify `generate_variations` event to be recorded after confirmation

---------

Co-authored-by: Fernando Marichal <contacto@fernandomarichal.com>
This commit is contained in:
Fernando Marichal 2023-04-12 16:37:23 -03:00 committed by GitHub
parent 8784d7c27a
commit 1187df9d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 287 additions and 161 deletions

View File

@ -7,7 +7,10 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { waitUntilElementIsPresent } from './utils';
import {
attachEventListenerToParentForChildren,
waitUntilElementIsPresent,
} from './utils';
/**
* Get the product data.
@ -244,9 +247,9 @@ const getDataForProductTabClickEvent = ( tabName: string ) => {
};
/**
* Initializes the product tabs Tracks events.
* Attaches the product tabs Tracks events.
*/
const initProductTabsTracks = () => {
const attachProductTabsTracks = () => {
const tabs = document.querySelectorAll( '.product_data_tabs > li' );
tabs.forEach( ( tab ) => {
@ -262,9 +265,9 @@ const initProductTabsTracks = () => {
};
/**
* Initializes the inventory tab Tracks events.
* Attaches the inventory tab Tracks events.
*/
const initInventoryTabTracks = () => {
const attachProductInventoryTabTracks = () => {
document
.querySelector( '#_manage_stock' )
?.addEventListener( 'click', ( event ) => {
@ -274,7 +277,7 @@ const initInventoryTabTracks = () => {
} );
document
.querySelector( '#_manage_stock_disabled' )
.querySelector( '#_manage_stock_disabled > a' )
?.addEventListener( 'click', () => {
recordEvent(
'product_manage_stock_disabled_store_settings_link_click'
@ -291,10 +294,225 @@ const initInventoryTabTracks = () => {
};
/**
* Initialize all product screen tracks.
* Attaches product tags tracks.
*/
const attachProductTagsTracks = () => {
function deleteTagEventListener(/* event: Event */) {
recordEvent( 'product_tags_delete', {
page: 'product',
tag_list_size:
document.querySelector( '.tagchecklist' )?.children.length || 0,
} );
}
export const initProductScreenTracks = () => {
function addTagsDeleteTracks() {
const tagsDeleteButtons = document.querySelectorAll(
'#product_tag .ntdelbutton'
);
tagsDeleteButtons.forEach( ( button ) => {
button.removeEventListener( 'click', deleteTagEventListener );
button.addEventListener( 'click', deleteTagEventListener );
} );
}
waitUntilElementIsPresent(
'#product_tag .tagchecklist',
addTagsDeleteTracks
);
document
.querySelector( '.tagadd' )
?.addEventListener( 'click', (/* event: Event */) => {
const tagInput = document.querySelector< HTMLInputElement >(
'#new-tag-product_tag'
);
if ( tagInput && tagInput.value && tagInput.value.length > 0 ) {
recordEvent( 'product_tags_add', {
page: 'product',
tag_string_length: tagInput.value.length,
tag_list_size:
( document.querySelector( '.tagchecklist' )?.children
.length || 0 ) + 1,
most_used: false,
} );
setTimeout( () => {
addTagsDeleteTracks();
}, 500 );
}
} );
function addMostUsedTagEventListener( event: Event ) {
recordEvent( 'product_tags_add', {
page: 'product',
tag_string_length: ( event.target as HTMLAnchorElement ).textContent
?.length,
tag_list_size:
document.querySelector( '.tagchecklist' )?.children.length || 0,
most_used: true,
} );
addTagsDeleteTracks();
}
function addMostUsedTagsTracks() {
const tagCloudLinks = document.querySelectorAll(
'#tagcloud-product_tag .tag-cloud-link'
);
tagCloudLinks.forEach( ( button ) => {
button.removeEventListener( 'click', addMostUsedTagEventListener );
button.addEventListener( 'click', addMostUsedTagEventListener );
} );
}
document
.querySelector( '.tagcloud-link' )
?.addEventListener( 'click', () => {
waitUntilElementIsPresent(
'#tagcloud-product_tag',
addMostUsedTagsTracks
);
} );
};
/**
* Attaches attributes tracks.
*/
const attachAttributesTracks = () => {
function addNewTermEventHandler() {
recordEvent( 'product_attributes_add_term', {
page: 'product',
} );
}
function addNewAttributeTermTracks() {
const addNewTermButtons = document.querySelectorAll(
'.woocommerce_attribute .add_new_attribute'
);
addNewTermButtons.forEach( ( button ) => {
button.removeEventListener( 'click', addNewTermEventHandler );
button.addEventListener( 'click', addNewTermEventHandler );
} );
}
addNewAttributeTermTracks();
document
.querySelector( '.add_attribute' )
?.addEventListener( 'click', () => {
setTimeout( () => {
addNewAttributeTermTracks();
}, 1000 );
} );
};
/**
* Attaches product attributes tracks.
*/
const attachProductAttributesTracks = () => {
const attributesCount = document.querySelectorAll(
'.woocommerce_attribute'
).length;
document
.querySelector( '.save_attributes' )
?.addEventListener( 'click', ( event ) => {
if (
event.target instanceof Element &&
event.target.classList.contains( 'disabled' )
) {
// skip in case the button is disabled
return;
}
const newAttributesCount = document.querySelectorAll(
'.woocommerce_attribute'
).length;
if ( newAttributesCount > attributesCount ) {
const local_attributes = [
...document.querySelectorAll(
'.woocommerce_attribute:not(.pa_glbattr)'
),
].map( ( attr ) => {
const terms =
(
attr.querySelector(
"[name^='attribute_values']"
) as HTMLTextAreaElement
)?.value.split( '|' ).length ?? 0;
return {
name: (
attr.querySelector(
'[name^="attribute_names"]'
) as HTMLInputElement
)?.value,
terms,
};
} );
recordEvent( 'product_attributes_add', {
page: 'product',
enable_archive: '',
default_sort_order: '',
local_attributes,
} );
}
} );
};
/**
* Attaches product variations tracks.
*/
const attachProductVariationsTracks = () => {
document
.querySelector(
'#variable_product_options_inner .variations-add-attributes-link'
)
?.addEventListener( 'click', () => {
recordEvent( 'product_variations_empty_state', {
action: 'add_attribute_link',
} );
} );
document
.querySelector(
'#variable_product_options_inner .variations-learn-more-link'
)
?.addEventListener( 'click', () => {
recordEvent( 'product_variations_empty_state', {
action: 'learn_more_link',
} );
} );
const variationsSection = '#variable_product_options';
// We attach the events in this way because the buttons are added dynamically.
attachEventListenerToParentForChildren( variationsSection, [
{
eventName: 'click',
childQuery: '.add_variation_manually',
callback: () => {
recordEvent( 'product_variations_buttons', {
action: 'add_variation_manually',
} );
},
},
{
eventName: 'change',
childQuery: '#field_to_edit',
callback: () => {
const selectElement = document.querySelector(
'#field_to_edit'
) as HTMLSelectElement;
// Get the index of the selected option
const selectedIndex = selectElement.selectedIndex;
recordEvent( 'product_variations_buttons', {
action: 'bulk_actions',
selected: selectElement.options[ selectedIndex ]?.value,
} );
},
},
] );
};
/**
* Attaches general product screen tracks.
*/
const attachProductScreenTracks = () => {
const initialPublishingData = getPublishingWidgetData();
document
@ -395,156 +613,6 @@ export const initProductScreenTracks = () => {
} );
} );
// Product tags
function deleteTagEventListener(/* event: Event */) {
recordEvent( 'product_tags_delete', {
page: 'product',
tag_list_size:
document.querySelector( '.tagchecklist' )?.children.length || 0,
} );
}
function addTagsDeleteTracks() {
const tagsDeleteButtons = document.querySelectorAll(
'#product_tag .ntdelbutton'
);
tagsDeleteButtons.forEach( ( button ) => {
button.removeEventListener( 'click', deleteTagEventListener );
button.addEventListener( 'click', deleteTagEventListener );
} );
}
waitUntilElementIsPresent(
'#product_tag .tagchecklist',
addTagsDeleteTracks
);
document
.querySelector( '.tagadd' )
?.addEventListener( 'click', (/* event: Event */) => {
const tagInput = document.querySelector< HTMLInputElement >(
'#new-tag-product_tag'
);
if ( tagInput && tagInput.value && tagInput.value.length > 0 ) {
recordEvent( 'product_tags_add', {
page: 'product',
tag_string_length: tagInput.value.length,
tag_list_size:
( document.querySelector( '.tagchecklist' )?.children
.length || 0 ) + 1,
most_used: false,
} );
setTimeout( () => {
addTagsDeleteTracks();
}, 500 );
}
} );
function addMostUsedTagEventListener( event: Event ) {
recordEvent( 'product_tags_add', {
page: 'product',
tag_string_length: ( event.target as HTMLAnchorElement ).textContent
?.length,
tag_list_size:
document.querySelector( '.tagchecklist' )?.children.length || 0,
most_used: true,
} );
addTagsDeleteTracks();
}
function addMostUsedTagsTracks() {
const tagCloudLinks = document.querySelectorAll(
'#tagcloud-product_tag .tag-cloud-link'
);
tagCloudLinks.forEach( ( button ) => {
button.removeEventListener( 'click', addMostUsedTagEventListener );
button.addEventListener( 'click', addMostUsedTagEventListener );
} );
}
document
.querySelector( '.tagcloud-link' )
?.addEventListener( 'click', () => {
waitUntilElementIsPresent(
'#tagcloud-product_tag',
addMostUsedTagsTracks
);
} );
// Attribute tracks.
function addNewTermEventHandler() {
recordEvent( 'product_attributes_add_term', {
page: 'product',
} );
}
function addNewAttributeTermTracks() {
const addNewTermButtons = document.querySelectorAll(
'.woocommerce_attribute .add_new_attribute'
);
addNewTermButtons.forEach( ( button ) => {
button.removeEventListener( 'click', addNewTermEventHandler );
button.addEventListener( 'click', addNewTermEventHandler );
} );
}
addNewAttributeTermTracks();
document
.querySelector( '.add_attribute' )
?.addEventListener( 'click', () => {
setTimeout( () => {
addNewAttributeTermTracks();
}, 1000 );
} );
const attributesCount = document.querySelectorAll(
'.woocommerce_attribute'
).length;
document
.querySelector( '.save_attributes' )
?.addEventListener( 'click', ( event ) => {
if (
event.target instanceof Element &&
event.target.classList.contains( 'disabled' )
) {
// skip in case the button is disabled
return;
}
const newAttributesCount = document.querySelectorAll(
'.woocommerce_attribute'
).length;
if ( newAttributesCount > attributesCount ) {
const local_attributes = [
...document.querySelectorAll(
'.woocommerce_attribute:not(.pa_glbattr)'
),
].map( ( attr ) => {
const terms =
(
attr.querySelector(
"[name^='attribute_values']"
) as HTMLTextAreaElement
)?.value.split( '|' ).length ?? 0;
return {
name: (
attr.querySelector(
'[name^="attribute_names"]'
) as HTMLInputElement
)?.value,
terms,
};
} );
recordEvent( 'product_attributes_add', {
page: 'product',
enable_archive: '',
default_sort_order: '',
local_attributes,
} );
}
} );
document
.querySelector(
'#woocommerce-product-updated-message-view-product__link'
@ -563,9 +631,19 @@ export const initProductScreenTracks = () => {
recordEvent( 'product_view_product_dismiss', getProductData() );
} );
} );
};
initProductTabsTracks();
initInventoryTabTracks();
/**
* Initialize all product screen tracks.
*/
export const initProductScreenTracks = () => {
attachAttributesTracks();
attachProductScreenTracks();
attachProductTagsTracks();
attachProductAttributesTracks();
attachProductVariationsTracks();
attachProductTabsTracks();
attachProductInventoryTabTracks();
};
export function addExitPageListener( pageId: string ) {

View File

@ -1,3 +1,39 @@
/**
* Attaches a click event listener to a parent element and calls a callback when one of the child elements,
* specified by the list of queries, is clicked. This allows handling events for child elements that may not
* exist in the DOM when the event listener is added.
*
* @param {string} parentQuery query of the parent element.
* @param {Array<Object>} children array of event, child query and callback pairs.
*/
export function attachEventListenerToParentForChildren(
parentQuery: string,
children: Array< {
eventName: 'click' | 'change';
childQuery: string;
callback: () => void;
} >
) {
const parent = document.querySelector( parentQuery );
if ( ! parent ) return;
const eventListener = ( event: Event ) => {
children.forEach( ( { eventName, childQuery, callback } ) => {
if (
event.type === eventName &&
( event.target as Element ).matches( childQuery )
) {
callback();
}
} );
};
children.forEach( ( { eventName } ) => {
parent.addEventListener( eventName, eventListener );
} );
}
/**
* Recursive function that waits up to 3 seconds until an element is found, then calls the callback.
*

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add tracks events to variations tab

View File

@ -1083,6 +1083,10 @@ jQuery( function ( $ ) {
} else {
wc_meta_boxes_product_variations_ajax.unblock();
}
window.wcTracks.recordEvent( 'product_variations_buttons', {
action: 'remove_variation',
} );
}
return false;
@ -1150,6 +1154,10 @@ jQuery( function ( $ ) {
}
}
);
window.wcTracks.recordEvent( 'product_variations_buttons', {
action: 'generate_variations',
} );
}
return false;

View File

@ -26,7 +26,7 @@ $arrow_img_url = WC_ADMIN_IMAGES_FOLDER_URL . '/product_data/no-variati
echo wp_kses_post(
sprintf(
/* translators: %1$s: url for attributes tab, %2$s: url for variable product documentation */
__( 'Add some attributes in the <a href="%1$s">Attributes</a> tab to generate variations. Make sure to check the <b>Used for variations</b> box. <a href="%2$s" target="_blank" rel="noreferrer">Learn more</a>', 'woocommerce' ),
__( 'Add some attributes in the <a class="variations-add-attributes-link" href="%1$s">Attributes</a> tab to generate variations. Make sure to check the <b>Used for variations</b> box. <a class="variations-learn-more-link" href="%2$s" target="_blank" rel="noreferrer">Learn more</a>', 'woocommerce' ),
esc_url( '#product_attributes' ),
esc_url( 'https://woocommerce.com/document/variable-product/' )
)