Commit 9067cc4d18d for woocommerce

commit 9067cc4d18da420a1be4d643c684e8fd19fd2d09
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Fri Sep 18 14:29:38 2026 +0200

    Add has_quality_badge to the marketplace product card click event (#68857)

    The card click event could not tell whether the card the merchant clicked
    showed the quality badge, so there was no way to compare click-through on
    badged and unbadged cards.

    `getVisibleQualityBadge()` now holds the decision the badge chip was making
    inline, and both the chip and the click event read it, so the property can
    never claim a badge the card did not render. Business service cards use a
    layout with no slot for the badge, so they always report false.

    The card link recorded the same event from two branches of its click
    handler; both now go through `handleCardClick`.

    Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/wccom-2941-product-card-click-quality-badge b/plugins/woocommerce/changelog/wccom-2941-product-card-click-quality-badge
new file mode 100644
index 00000000000..3c961ae00a7
--- /dev/null
+++ b/plugins/woocommerce/changelog/wccom-2941-product-card-click-quality-badge
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Marketplace: report whether a product card shows the quality badge in the card click tracking event.
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
index 57e881c4166..fbf93835341 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
@@ -14,7 +14,9 @@ import { useState, useContext, useRef } from '@wordpress/element';
  */
 import './product-card.scss';
 import ProductCardFooter from './product-card-footer';
-import QualityBadge from '../quality-badge/quality-badge';
+import QualityBadge, {
+	getVisibleQualityBadge,
+} from '../quality-badge/quality-badge';
 import {
 	Product,
 	ProductCardType,
@@ -82,6 +84,11 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
 		iamSettings?.product_previews === 'modal' &&
 		! isTheme &&
 		! isBusinessService;
+	// Business service cards use a layout with no slot for the badge.
+	const showsQualityBadge =
+		! isLoading &&
+		! isBusinessService &&
+		getVisibleQualityBadge( props.product, iamSettings ) !== null;

 	const showVendor = ! isCompact && ! isLoading;
 	const showVendorLoading = ! isCompact && isLoading;
@@ -192,6 +199,7 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
 			product_name: product.title,
 			vendor: product.vendorName,
 			product_type: type,
+			has_quality_badge: showsQualityBadge,
 		} );

 		if ( shouldShowPreview ) {
@@ -247,15 +255,8 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
 				onClick={ ( e ) => {
 					if ( shouldShowPreview ) {
 						e.preventDefault();
-						handleCardClick();
-					} else {
-						recordTracksEvent( 'marketplace_product_card_clicked', {
-							product_id: product.id,
-							product_name: product.title,
-							vendor: product.vendorName,
-							product_type: type,
-						} );
 					}
+					handleCardClick();
 				} }
 			>
 				{ isLoading ? ' ' : product.title }
@@ -298,7 +299,7 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
 	};

 	const qualityBadge =
-		! isLoading && props.product ? (
+		showsQualityBadge && props.product ? (
 			<QualityBadge product={ props.product } />
 		) : null;

diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
index e7f02c2038c..faac2fdf821 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
@@ -1,8 +1,9 @@
 /**
  * External dependencies
  */
-import { render } from '@testing-library/react';
+import { fireEvent, render } from '@testing-library/react';
 import React from 'react';
+import { queueRecordEvent } from '@woocommerce/tracks';

 jest.mock( '@woocommerce/navigation', () => ( {
 	getNewPath: jest.fn( () => '/new-path' ),
@@ -15,6 +16,12 @@ jest.mock( '@woocommerce/tracks', () => ( {
 	queueRecordEvent: jest.fn(),
 } ) );

+// The preview modal loads its own data; these tests only care about the card.
+jest.mock(
+	'../../product-preview-modal/product-preview-modal',
+	() => () => null
+);
+
 jest.mock( '@woocommerce/data', () => ( {
 	useUser: jest.fn( () => ( {
 		user: null,
@@ -66,12 +73,18 @@ const product: Product = {
 	hasQualityBadge: true,
 };

-function renderCard( cardType: ProductCardType ) {
+function renderCard(
+	cardType: ProductCardType,
+	productOverrides: Partial< Product > = {},
+	contextOverrides: Partial< MarketplaceContextType > = {}
+) {
 	return render(
-		<MarketplaceContext.Provider value={ context }>
+		<MarketplaceContext.Provider
+			value={ { ...context, ...contextOverrides } }
+		>
 			<ProductCard
 				type={ ProductType.extension }
-				product={ product }
+				product={ { ...product, ...productOverrides } }
 				cardType={ cardType }
 				tracksData={ { position: 1 } }
 			/>
@@ -122,16 +135,9 @@ describe( 'ProductCard quality badge placement', () => {
 	} );

 	it( 'renders no badge when the product does not have one', () => {
-		const { container } = render(
-			<MarketplaceContext.Provider value={ context }>
-				<ProductCard
-					type={ ProductType.extension }
-					product={ { ...product, hasQualityBadge: false } }
-					cardType={ ProductCardType.compact }
-					tracksData={ {} }
-				/>
-			</MarketplaceContext.Provider>
-		);
+		const { container } = renderCard( ProductCardType.compact, {
+			hasQualityBadge: false,
+		} );

 		expect( getBadge( container ) ).toBeNull();
 	} );
@@ -150,16 +156,7 @@ describe( 'ProductCard sponsored label', () => {
 		cardType: ProductCardType,
 		overrides: Partial< Product > = {}
 	) {
-		return render(
-			<MarketplaceContext.Provider value={ context }>
-				<ProductCard
-					type={ ProductType.extension }
-					product={ { ...sponsoredProduct, ...overrides } }
-					cardType={ cardType }
-					tracksData={ {} }
-				/>
-			</MarketplaceContext.Provider>
-		);
+		return renderCard( cardType, { ...sponsoredProduct, ...overrides } );
 	}

 	function getSponsoredLabel( view: ReturnType< typeof render > ) {
@@ -218,3 +215,91 @@ describe( 'ProductCard sponsored label', () => {
 		).toBeNull();
 	} );
 } );
+
+describe( 'ProductCard click tracking', () => {
+	beforeEach( () => {
+		jest.mocked( queueRecordEvent ).mockClear();
+	} );
+
+	function clickCard(
+		productOverrides: Partial< Product > = {},
+		contextOverrides: Partial< MarketplaceContextType > = {}
+	) {
+		const view = renderCard(
+			ProductCardType.regular,
+			productOverrides,
+			contextOverrides
+		);
+
+		fireEvent.click( view.getByRole( 'link' ) );
+	}
+
+	// A click records exactly one event, and the mock is cleared before each test.
+	function cardClickEvents() {
+		return jest.mocked( queueRecordEvent ).mock.calls;
+	}
+
+	it( 'reports the badge when the card shows one', () => {
+		clickCard();
+
+		expect( cardClickEvents() ).toEqual( [
+			[
+				'marketplace_product_card_clicked',
+				expect.objectContaining( {
+					product_id: 1,
+					has_quality_badge: true,
+				} ),
+			],
+		] );
+	} );
+
+	it( 'reports no badge when the product does not have one', () => {
+		clickCard( { hasQualityBadge: false } );
+
+		expect( cardClickEvents()[ 0 ][ 1 ] ).toMatchObject( {
+			has_quality_badge: false,
+		} );
+	} );
+
+	it( 'reports no badge when the badge is disabled for the store', () => {
+		clickCard(
+			{},
+			{
+				iamSettings: {
+					...context.iamSettings,
+					quality_badge: {
+						...context.iamSettings.quality_badge,
+						enabled: false,
+					},
+				},
+			}
+		);
+
+		expect( cardClickEvents()[ 0 ][ 1 ] ).toMatchObject( {
+			has_quality_badge: false,
+		} );
+	} );
+
+	it( 'reports no badge on business service cards, which cannot show one', () => {
+		clickCard( { type: ProductType.businessService } );
+
+		expect( cardClickEvents()[ 0 ][ 1 ] ).toMatchObject( {
+			product_type: ProductType.businessService,
+			has_quality_badge: false,
+		} );
+	} );
+
+	it( 'records the click once when the card opens a preview modal', () => {
+		clickCard(
+			{},
+			{
+				iamSettings: {
+					...context.iamSettings,
+					product_previews: 'modal',
+				},
+			}
+		);
+
+		expect( cardClickEvents() ).toHaveLength( 1 );
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.tsx
index 9a13da0e5de..2e5944f7ced 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.tsx
@@ -13,6 +13,7 @@ import { recordEvent } from '@woocommerce/tracks';
 import './quality-badge.scss';
 import { Product } from '../product-list/types';
 import { MarketplaceContext } from '../../contexts/marketplace-context';
+import { MarketplaceContextType } from '../../contexts/types';

 /**
  * Seal-with-checkmark icon, kept identical to the badge icon on WooCommerce.com.
@@ -147,6 +148,24 @@ export function QualityBadgePopover( props: {
 	);
 }

+/**
+ * The badge settings when a product card shows the badge, otherwise null.
+ * Product card tracking reads this too, so an event never reports a badge the
+ * merchant could not see.
+ */
+export function getVisibleQualityBadge(
+	product: Product | undefined,
+	iamSettings: MarketplaceContextType[ 'iamSettings' ] | undefined
+) {
+	const badge = iamSettings?.quality_badge;
+
+	if ( ! badge?.enabled || ! badge.label || ! product?.hasQualityBadge ) {
+		return null;
+	}
+
+	return { ...badge, label: badge.label };
+}
+
 /**
  * Quality badge chip shown on product cards. Whether it renders and with what
  * copy is fully driven by the WooCommerce.com API: the per-product flag comes
@@ -158,13 +177,9 @@ export default function QualityBadge( props: { product: Product } ) {
 	const [ isOpen, setIsOpen ] = useState( false );
 	const [ anchor, setAnchor ] = useState< HTMLButtonElement | null >( null );

-	const badge = iamSettings?.quality_badge;
+	const badge = getVisibleQualityBadge( props.product, iamSettings );

-	if (
-		! badge?.enabled ||
-		! badge.label ||
-		! props.product.hasQualityBadge
-	) {
+	if ( ! badge ) {
 		return null;
 	}