-
Notifications
You must be signed in to change notification settings - Fork 891
Premium Analytics: add Stats traffic queries #49777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: patch | ||
| Type: added | ||
|
|
||
| Stats: Add core proxy query definitions. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsLocationsQuery } from '../stats-locations-query'; | ||
| import { statsTopPostsQuery } from '../stats-top-posts-query'; | ||
| import type { StatsReportParams } from '../stats-query'; | ||
|
|
||
| describe( 'Stats query factories', () => { | ||
| it( 'disables report queries until a date range is available', () => { | ||
| expect( statsTopPostsQuery( {} as StatsReportParams ).enabled ).toBe( false ); | ||
| } ); | ||
|
|
||
| it( 'builds location query keys from geoMode', () => { | ||
| const query = statsLocationsQuery( { | ||
| from: '2026-06-16', | ||
| to: '2026-06-16', | ||
| interval: 'day', | ||
| geoMode: 'city', | ||
| } ); | ||
|
|
||
| expect( query.enabled ).toBe( true ); | ||
| expect( query.queryKey ).toEqual( [ | ||
| 'stats', | ||
| 'locations-city', | ||
| '1.1', | ||
| 'stats/location-views/city', | ||
| 'GET', | ||
| expect.objectContaining( { date: '2026-06-16' } ), | ||
| undefined, | ||
| 'locations', | ||
| ] ); | ||
| } ); | ||
|
|
||
| it( 'requests summarized data for multi-day report ranges', () => { | ||
| const query = statsTopPostsQuery( { | ||
| from: '2026-06-01', | ||
| to: '2026-06-07', | ||
| interval: 'day', | ||
| } ); | ||
|
|
||
| expect( query.queryKey ).toEqual( | ||
| expect.arrayContaining( [ | ||
| expect.objectContaining( { | ||
| date: '2026-06-07', | ||
| start_date: '2026-06-01', | ||
| days: 7, | ||
| summarize: 1, | ||
| } ), | ||
| ] ) | ||
| ); | ||
| } ); | ||
|
|
||
| it( 'preserves explicit summarize params', () => { | ||
| const query = statsTopPostsQuery( { | ||
| from: '2026-06-01', | ||
| to: '2026-06-07', | ||
| interval: 'day', | ||
| summarize: false, | ||
| } ); | ||
|
|
||
| expect( query.queryKey ).toEqual( | ||
| expect.arrayContaining( [ | ||
| expect.objectContaining( { | ||
| date: '2026-06-07', | ||
| days: 7, | ||
| summarize: false, | ||
| } ), | ||
| ] ) | ||
| ); | ||
| } ); | ||
| } ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsClicksQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'clicks', 'stats/clicks', params, 'clicks' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsCountryViewsQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'country-views', 'stats/country-views', params, 'locations' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsFileDownloadsQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'file-downloads', 'stats/file-downloads', params, 'fileDownloads' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsLocationsQuery = ( | ||
| params: StatsReportParams & { geoMode?: 'country' | 'region' | 'city' } | ||
| ) => { | ||
| const geoMode = params.geoMode ?? 'country'; | ||
|
|
||
| return statsReportQuery( | ||
| `locations-${ geoMode }`, | ||
| `stats/location-views/${ geoMode }`, | ||
| params, | ||
| 'locations' | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { fetchStatsProxy, type StatsProxyMethod, type StatsProxyVersion } from '../api'; | ||
| import { | ||
| sanitizeStatsClicksResponse, | ||
| sanitizeStatsFileDownloadsResponse, | ||
| sanitizeStatsLocationsResponse, | ||
| sanitizeStatsPassthroughResponse, | ||
| sanitizeStatsReferrersResponse, | ||
| sanitizeStatsSearchTermsResponse, | ||
| sanitizeStatsSiteResponse, | ||
| sanitizeStatsTopAuthorsResponse, | ||
| sanitizeStatsTopPostsResponse, | ||
| sanitizeStatsVideoPlaysResponse, | ||
| } from '../processing/stats'; | ||
| import { | ||
| reportParamsToStatsQueryParams, | ||
| statsQueryParamsToApiParams, | ||
| type StatsQueryParams, | ||
| } from '../utils/stats-params'; | ||
| import type { ReportParams } from '../utils/search'; | ||
| import type { UseQueryOptions } from '@tanstack/react-query'; | ||
|
|
||
| export type StatsReportParams = ReportParams & StatsQueryParams; | ||
| type StatsSanitizer< TData = unknown > = ( response: unknown, params?: StatsQueryParams ) => TData; | ||
|
|
||
| const statsSanitizers = { | ||
| passthrough: sanitizeStatsPassthroughResponse, | ||
| site: sanitizeStatsSiteResponse, | ||
| topPosts: sanitizeStatsTopPostsResponse, | ||
| referrers: sanitizeStatsReferrersResponse, | ||
| clicks: sanitizeStatsClicksResponse, | ||
| searchTerms: sanitizeStatsSearchTermsResponse, | ||
| fileDownloads: sanitizeStatsFileDownloadsResponse, | ||
| topAuthors: sanitizeStatsTopAuthorsResponse, | ||
| locations: sanitizeStatsLocationsResponse, | ||
| videoPlays: sanitizeStatsVideoPlaysResponse, | ||
| } satisfies Record< string, StatsSanitizer >; | ||
|
|
||
| export type StatsSanitizerKey = keyof typeof statsSanitizers; | ||
| type StatsSanitizerData = ReturnType< ( typeof statsSanitizers )[ StatsSanitizerKey ] >; | ||
|
|
||
| export type StatsQueryConfig = { | ||
| name: string; | ||
| version: StatsProxyVersion; | ||
| endpoint: string; | ||
| params?: StatsQueryParams; | ||
| method?: StatsProxyMethod; | ||
| body?: unknown; | ||
| sanitizer?: StatsSanitizerKey; | ||
| enabled?: boolean; | ||
| }; | ||
|
|
||
| export function statsProxyQuery( config: StatsQueryConfig ): UseQueryOptions< StatsSanitizerData > { | ||
| const { name, version, endpoint, params, method = 'GET', body, enabled = true } = config; | ||
| const sanitizer = config.sanitizer ?? 'passthrough'; | ||
| const apiParams = statsQueryParamsToApiParams( params ); | ||
|
|
||
| return { | ||
| queryKey: [ 'stats', name, version, endpoint, method, apiParams, body, sanitizer ], | ||
| queryFn: async () => { | ||
| const response = await fetchStatsProxy( { | ||
| version, | ||
| endpoint, | ||
| params: apiParams, | ||
| method, | ||
| body, | ||
| } ); | ||
| return statsSanitizers[ sanitizer ]( response, apiParams ); | ||
| }, | ||
| enabled, | ||
| placeholderData: previousData => previousData, | ||
| }; | ||
| } | ||
|
|
||
| export function statsReportQuery( | ||
| name: string, | ||
| endpoint: string, | ||
| params: StatsReportParams, | ||
| sanitizer: StatsSanitizerKey, | ||
| version: StatsProxyVersion = '1.1' | ||
| ): UseQueryOptions< StatsSanitizerData > { | ||
| const statsParams = reportParamsToStatsQueryParams( params ); | ||
| const reportParams = { | ||
| ...statsParams, | ||
| ...( statsParams.summarize === undefined && | ||
| typeof statsParams.days === 'number' && | ||
| statsParams.days > 1 | ||
| ? { summarize: 1 } | ||
| : {} ), | ||
| }; | ||
|
|
||
| return statsProxyQuery( { | ||
| name, | ||
| version, | ||
| endpoint, | ||
| params: reportParams, | ||
| sanitizer, | ||
| enabled: !! ( reportParams.end_date || reportParams.date || reportParams.start_date ), | ||
| } ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsReferrersQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'referrers', 'stats/referrers', params, 'referrers' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsSearchTermsQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'search-terms', 'stats/search-terms', params, 'searchTerms' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsProxyQuery } from './stats-query'; | ||
| import type { StatsQueryParams } from '../utils/stats-params'; | ||
|
|
||
| export const statsSiteQuery = ( params: StatsQueryParams = {} ) => | ||
| statsProxyQuery( { | ||
| name: 'site', | ||
| version: '1.1', | ||
| endpoint: 'stats', | ||
| params, | ||
| sanitizer: 'site', | ||
| } ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsTopAuthorsQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'top-authors', 'stats/top-authors', params, 'topAuthors' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsTopPostsQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'top-posts', 'stats/top-posts', params, 'topPosts' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { statsReportQuery, type StatsReportParams } from './stats-query'; | ||
|
|
||
| export const statsVideoPlaysQuery = ( params: StatsReportParams ) => | ||
| statsReportQuery( 'video-plays', 'stats/video-plays', params, 'videoPlays' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,25 +25,17 @@ type StatsQueryParamInput = Partial< ReportParams > & { | |
| [ key: string ]: unknown; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Can we make
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partially addressed: the repeated explicit field list is gone and the output still uses the explicit |
||
| } & Partial< StatsQueryParamFields >; | ||
|
|
||
| type ReportOnlyParam = keyof ReportParams | 'geoMode' | 'utmParams' | 'deviceProperty'; | ||
|
|
||
| const reportOnlyKeys: ReportOnlyParam[] = [ | ||
| 'from', | ||
| 'to', | ||
| 'interval', | ||
| 'preset', | ||
| 'compare_from', | ||
| 'compare_to', | ||
| 'compare_preset', | ||
| 'comp', | ||
| 'filters', | ||
| 'section', | ||
| 'date_type', | ||
| 'view', | ||
| 'geoMode', | ||
| 'utmParams', | ||
| 'deviceProperty', | ||
| ]; | ||
| const statsParamKeys = [ | ||
| 'period', | ||
| 'end_date', | ||
| 'date', | ||
| 'start_date', | ||
| 'days', | ||
| 'num', | ||
| 'max', | ||
| 'summarize', | ||
| 'complete_stats', | ||
| ] as const satisfies Array< keyof StatsQueryParamFields >; | ||
|
|
||
| function datePart( value?: string ) { | ||
| return value?.split( 'T' )[ 0 ]; | ||
|
|
@@ -69,11 +61,11 @@ export function getStatsPeriodFromInterval( interval?: string ): StatsPeriod { | |
| export function reportParamsToStatsQueryParams( | ||
| params: StatsQueryParamInput = {} | ||
| ): StatsQueryParams { | ||
| const statsParams = { ...params }; | ||
|
|
||
| reportOnlyKeys.forEach( key => { | ||
| delete statsParams[ key ]; | ||
| } ); | ||
| const statsParams = Object.fromEntries( | ||
| statsParamKeys | ||
| .filter( key => params[ key ] !== undefined && params[ key ] !== null ) | ||
| .map( key => [ key, params[ key ] ] ) | ||
| ) as StatsQueryParams; | ||
|
|
||
| const from = datePart( params.from ); | ||
| const to = datePart( params.to ); | ||
|
|
@@ -85,7 +77,7 @@ export function reportParamsToStatsQueryParams( | |
| ( startDate && endDate ? getDaysBetweenInclusive( startDate, endDate ) : undefined ); | ||
|
|
||
| return { | ||
| ...( statsParams as StatsQueryParams ), | ||
| ...statsParams, | ||
| period, | ||
| ...( endDate ? { end_date: endDate } : {} ), | ||
| ...( startDate ? { start_date: startDate } : {} ), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
statsReportQuery is a report-specific wrapper around statsProxyQuery.
It accepts Woo/report-style date params like from, to, interval, converts them into Stats params like period, start_date, end_date/API date,
computes days, enables the query only when a date range exists, and now defaults multi-day ranges to summarize: 1.