Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Stats: Add email breakdown hooks.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const statsHookNames = [
'useStatsAppReferrersSpam',
'useStatsAppReferrersMarkSpamMutation',
'useStatsAppReferrersUnmarkSpamMutation',
'useStatsEmailOpensBreakdown',
'useStatsEmailClicksBreakdown',
] as const satisfies ReadonlyArray< keyof typeof dataPackage >;

describe( 'Stats public hook names', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ export type {
StatsAppReferrersSpamMutationResponse,
StatsAppReferrersSpamResponse,
} from './use-stats-app-referrers-spam';
export {
useStatsEmailOpensBreakdown,
useStatsEmailClicksBreakdown,
type StatsEmailBreakdown,
type StatsEmailClicksBreakdown,
type StatsEmailOpensBreakdown,
} from './use-stats-email-breakdown';
export type { UseStatsOptions } from './use-stats-report';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Internal dependencies
*/
import {
statsEmailClicksBreakdownQuery,
statsEmailOpensBreakdownQuery,
type StatsEmailBreakdown,
type StatsEmailClicksBreakdown,
type StatsEmailOpensBreakdown,
} from '../queries/stats-email-breakdown-query';
import { useStatsQuery } from './use-stats-query';
import type { UseStatsOptions } from './use-stats-report';

export function useStatsEmailOpensBreakdown(
postId: number,
breakdown: StatsEmailOpensBreakdown,
options?: UseStatsOptions
) {
return useStatsQuery( statsEmailOpensBreakdownQuery( postId, breakdown ), options );
}

export function useStatsEmailClicksBreakdown(
postId: number,
breakdown: StatsEmailClicksBreakdown,
options?: UseStatsOptions
) {
return useStatsQuery( statsEmailClicksBreakdownQuery( postId, breakdown ), options );
}

export type { StatsEmailBreakdown, StatsEmailClicksBreakdown, StatsEmailOpensBreakdown };
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export type {
StatsAppReferrersSpamMutationResponse,
StatsAppReferrersSpamResponse,
} from './hooks/use-stats-app-referrers-spam';
export {
useStatsEmailOpensBreakdown,
useStatsEmailClicksBreakdown,
type StatsEmailBreakdown,
type StatsEmailClicksBreakdown,
type StatsEmailOpensBreakdown,
} from './hooks/use-stats-email-breakdown';
export type { UseStatsOptions } from './hooks/use-stats-report';
export { prefetchReport } from './prefetch';
export {
Expand Down Expand Up @@ -174,6 +181,7 @@ export type {
StatsCommentsRawFollowData,
StatsCommentsRawPost,
StatsCommentsRawResponse,
StatsEmailBreakdownItem,
StatsFileDownloadsItem,
StatsFollowersItem,
StatsFollowersRawItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { statsArchivesQuery } from '../stats-archives-query';
import { statsCommentFollowersQuery } from '../stats-comment-followers-query';
import { statsCommentsQuery } from '../stats-comments-query';
import { statsDevicesQuery } from '../stats-devices-query';
import {
statsEmailClicksBreakdownQuery,
statsEmailOpensBreakdownQuery,
} from '../stats-email-breakdown-query';
import { statsFollowersQuery } from '../stats-followers-query';
import { STATS_HIGHLIGHTS_STALE_TIME, statsHighlightsQuery } from '../stats-highlights-query';
import { statsInsightsQuery } from '../stats-insights-query';
Expand Down Expand Up @@ -71,6 +75,42 @@ describe( 'Stats query factories', () => {
expect( statsPostQuery( { postId: 0 } ).enabled ).toBe( false );
} );

it( 'builds all-time email opens breakdown query keys without query params', () => {
const query = statsEmailOpensBreakdownQuery( 41, 'country' );

expect( query.enabled ).toBe( true );
expect( query.queryKey ).toEqual( [
'stats',
'email-opens-country',
'1.1',
'stats/opens/emails/41/country',
'GET',
{},
undefined,
'emailBreakdown',
] );
} );

it( 'builds all-time email clicks breakdown query keys per breakdown dimension', () => {
const query = statsEmailClicksBreakdownQuery( 41, 'user-content-link' );

expect( query.queryKey ).toEqual( [
'stats',
'email-clicks-user-content-link',
'1.1',
'stats/clicks/emails/41/user-content-link',
'GET',
{},
undefined,
'emailBreakdown',
] );
} );

it( 'disables email breakdown queries until a positive post ID is available', () => {
expect( statsEmailOpensBreakdownQuery( 0, 'client' ).enabled ).toBe( false );
expect( statsEmailClicksBreakdownQuery( -1, 'link' ).enabled ).toBe( false );
} );

it( 'includes filter_by_country in query params when provided', () => {
const query = statsLocationsQuery( {
from: '2026-06-16',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ export type {
StatsAppReferrersSpamMutationResponse,
StatsAppReferrersSpamResponse,
} from './stats-app-referrers-spam-query';
export {
statsEmailOpensBreakdownQuery,
statsEmailClicksBreakdownQuery,
type StatsEmailClicksBreakdown,
type StatsEmailOpensBreakdown,
} from './stats-email-breakdown-query';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Internal dependencies
*/
import { statsProxyQuery } from './stats-query';
import type { StatsReportQueryOptions } from './stats-query';
import type { StatsEmailBreakdownItem, StatsNormalizedReport } from '../processing/stats';

export type StatsEmailOpensBreakdown = 'client' | 'device' | 'country' | 'rate';
export type StatsEmailClicksBreakdown = StatsEmailOpensBreakdown | 'link' | 'user-content-link';
export type StatsEmailBreakdown = StatsNormalizedReport< StatsEmailBreakdownItem >;

const hasValidPostId = ( postId: number ) => Number.isInteger( postId ) && postId > 0;

// The all-time breakdown endpoints are keyed entirely by post ID and breakdown; unlike the
// timeline endpoint they take no period/date query params (see Calypso PERIOD_ALL_TIME handling).
export const statsEmailOpensBreakdownQuery = (
postId: number,
breakdown: StatsEmailOpensBreakdown
): StatsReportQueryOptions< 'emailBreakdown' > =>
statsProxyQuery( {
name: `email-opens-${ breakdown }`,
version: '1.1',
endpoint: `stats/opens/emails/${ postId }/${ breakdown }`,
sanitizer: 'emailBreakdown',
enabled: hasValidPostId( postId ),
} );

export const statsEmailClicksBreakdownQuery = (
postId: number,
breakdown: StatsEmailClicksBreakdown
): StatsReportQueryOptions< 'emailBreakdown' > =>
statsProxyQuery( {
name: `email-clicks-${ breakdown }`,
version: '1.1',
endpoint: `stats/clicks/emails/${ postId }/${ breakdown }`,
sanitizer: 'emailBreakdown',
enabled: hasValidPostId( postId ),
} );
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
sanitizeStatsTagsResponse,
sanitizeStatsTimeSeriesResponse,
sanitizeStatsPublicizeResponse,
sanitizeStatsEmailBreakdownResponse,
sanitizeStatsPassthroughResponse,
sanitizeStatsPostResponse,
sanitizeStatsReferrersResponse,
Expand Down Expand Up @@ -74,6 +75,7 @@ const statsSanitizers = {
publicize: sanitizeStatsPublicizeResponse,
wordAdsStats: sanitizeStatsWordAdsStatsResponse,
wordAdsEarnings: sanitizeStatsWordAdsEarningsResponse,
emailBreakdown: sanitizeStatsEmailBreakdownResponse,
} satisfies Record< string, StatsSanitizer >;

export type StatsSanitizerKey = keyof typeof statsSanitizers;
Expand Down
Loading