/** * WordPress dependencies */ import { Button } from '@safe-wordpress/components'; import { useDispatch, useSelect, type SelectFunction, } from '@safe-wordpress/data'; import { createInterpolateElement } from '@safe-wordpress/element'; import { _n, _x, sprintf } from '@safe-wordpress/i18n'; /** * External dependencies */ import { isFunction } from 'lodash'; import { HelpIcon } from '@nab/components'; import { store as NAB_DATA, usePageAttribute } from '@nab/data'; import { formatI18nDate } from '@nab/date'; import { store as NAB_EXPERIMENTS } from '@nab/experiments'; import { computeDuration, getLetter, hasHead } from '@nab/utils'; import type { Alternative, AlternativeId, AlternativeTrackingData, ECommercePlugin, Experiment, ExperimentId, GoalId, Maybe, Opportunity, PostId, Url, } from '@nab/types'; /** * Internal dependencies */ import './style.scss'; import { useExperimentAttribute } from '../hooks'; import { getMoneyLabel } from '../utils'; import { TypicalSampleSize } from './typical-sample-size'; import type { ControlAttributes as PostControlAttributes } from '../../../../../../../packages/experiment-library/post/types'; import type { ControlAttributes as UrlControlAttributes } from '../../../../../../../packages/experiment-library/url/types'; type ExperimentStatus = 'running' | 'finished'; type Results = | { readonly type: 'loading' } | { readonly type: 'collecting-results' } | ( { readonly type: 'winner' } & DataDetails & Winner ) | ( { readonly type: 'possible-winner' } & DataDetails & Winner ) | ( { readonly type: 'no-winner' } & DataDetails ); type Winner = { readonly winner: { readonly id: AlternativeId; readonly name: string; readonly isLastApplied: boolean; }; }; type DataDetails = { readonly alternativeCount: number; readonly minConfidence: number; readonly confidence: number; readonly uplift: number; readonly estimatedImpact?: { readonly value: string; readonly uplift?: number; }; }; type SummaryContent = { readonly eyebrow: string; readonly title: string; readonly subtitle: string | JSX.Element; readonly description: string | JSX.Element; readonly icon: JSX.Element; readonly primaryAction?: { readonly label: string; readonly isBusy: boolean; readonly isDisabled: boolean; readonly onClick: () => void; }; readonly secondaryAction?: { readonly label: string; readonly isBusy: boolean; readonly isDisabled: boolean; readonly onClick: () => void; }; }; export const Summary = (): JSX.Element => { const { id: experimentId, status: experimentStatus, startDate, endDate, hasValueEstimate, } = useExperiment(); const results = useResults(); const winner = getWinner( results ); const [ _, doApplyWinner ] = usePageAttribute( 'results/alternativeToApply', false ); const applyWinner = useCanApplyAlternativeWinner( experimentId, winner ) ? () => doApplyWinner( winner ) : undefined; const { stopExperiment: doStopTest } = useDispatch( NAB_DATA ); const stopTest = useCanStopTest() ? doStopTest : undefined; const [ isStoppingTest ] = usePageAttribute( 'editor/isExperimentBeingStopped', false ); const opportunity = useOpportunity( experimentId ); const { generateExperimentSuggestions } = useDispatch( NAB_DATA ); const doCreateFollowUpTest = () => opportunity && generateExperimentSuggestions( opportunity ); const createFollowUpTest = useCanCreateFollowUpTest() && !! opportunity ? doCreateFollowUpTest : undefined; const [ isCreatingFollowUpTest ] = usePageAttribute( 'results/isCreatingFollowUpTest', false ); const areActionsDisabled = isStoppingTest || isCreatingFollowUpTest; const args = { areActionsDisabled, isCreatingFollowUpTest, isStoppingTest, isWinnerLastApplied: getIsWinnerLastApplied( results ), results, status: experimentStatus, applyWinner, createFollowUpTest, stopTest, }; const content = 'running' === experimentStatus ? getRunningSummaryContent( args ) : getFinishedSummaryContent( args ); return (
{ ! content ? ( ) : ( <>

{ content.eyebrow }

{ content.title }

{ content.subtitle }

{ content.description }

{ hasEstimatedImpact( results ) && ( ) }
{ ( content.primaryAction || content.secondaryAction ) && (
{ content.secondaryAction && ( ) } { content.primaryAction && ( ) }
) } ) }
); }; type GetSummaryContentArgs = { readonly areActionsDisabled: boolean; readonly isCreatingFollowUpTest: boolean; readonly isStoppingTest: boolean; readonly isWinnerLastApplied: boolean; readonly results: Results; readonly applyWinner?: () => void; readonly createFollowUpTest?: () => void; readonly stopTest?: () => void; }; const getRunningSummaryContent = ( args: GetSummaryContentArgs ): Maybe< SummaryContent > => { switch ( args.results.type ) { case 'loading': return undefined; case 'winner': { const alternativeName = args.results.winner.name; const alternativeId = args.results.winner.id; return { eyebrow: _x( 'Winner found', 'text', 'nelio-ab-testing' ), title: sprintf( /* translators: %s: Variant name (as in “Variant X”). */ _x( '%s is the clear winner', 'text', 'nelio-ab-testing' ), alternativeName ), subtitle: getUpliftHeadline( args.results, 'running' ), description: sprintf( '%1$s %2$s', // eslint-disable-next-line no-nested-ternary 'control' === alternativeId ? 2 === args.results.alternativeCount ? _x( 'The control is outperforming variant B.', 'text', 'nelio-ab-testing' ) : _x( 'The control is outperforming the other variants.', 'text', 'nelio-ab-testing' ) : sprintf( /* translators: %s: Variant name (as in “Variant X”). */ _x( '%s is outperforming the control.', 'text', 'nelio-ab-testing' ), alternativeName ), _x( 'We recommend stopping the test and applying the winning variant to all visitors.', 'text', 'nelio-ab-testing' ) ), icon: , primaryAction: args.stopTest ? { isBusy: args.isStoppingTest, isDisabled: args.areActionsDisabled && ! args.isStoppingTest, label: _x( 'Stop Test Now', 'command', 'nelio-ab-testing' ), onClick: args.stopTest, } : undefined, }; } case 'possible-winner': { const alternativeName = args.results.winner.name; return { eyebrow: _x( 'Promising result', 'text', 'nelio-ab-testing' ), title: sprintf( /* translators: %s: Variant name. */ _x( '%s is currently leading', 'text', 'nelio-ab-testing' ), alternativeName ), subtitle: getUpliftHeadline( args.results, 'running' ), description: _x( 'This variant is currently ahead, but more data is needed before declaring a winner.', 'text', 'nelio-ab-testing' ), icon: , }; } case 'no-winner': return { eyebrow: _x( 'No significant difference', 'text', 'nelio-ab-testing' ), title: _x( 'No clear winner yet', 'text', 'nelio-ab-testing' ), subtitle: _x( 'No variant is clearly outperforming the control', 'text', 'nelio-ab-testing' ), description: _x( 'The test hasn’t identified a clear winner yet. Keep it running or try a different hypothesis.', 'text', 'nelio-ab-testing' ), icon: , }; case 'collecting-results': return { eyebrow: _x( 'Collecting results', 'text', 'nelio-ab-testing' ), title: _x( 'Not enough data yet', 'text', 'nelio-ab-testing' ), subtitle: _x( 'The test is still collecting data', 'text', 'nelio-ab-testing' ), description: _x( 'Keep the test running until there’s enough data to evaluate the variants reliably.', 'text', 'nelio-ab-testing' ), icon: , }; } }; const getFinishedSummaryContent = ( args: { readonly results: Results; } & GetSummaryContentArgs ): Maybe< SummaryContent > => { switch ( args.results.type ) { case 'loading': return undefined; case 'winner': { const alternativeName = args.results.winner.name; const alternativeId = args.results.winner.id; return { eyebrow: _x( 'Winner found', 'text', 'nelio-ab-testing' ), title: sprintf( /* translators: %s: Variant name (as in “Variant X”). */ _x( '%s won', 'text', 'nelio-ab-testing' ), alternativeName ), subtitle: getUpliftHeadline( args.results, 'finished' ), description: sprintf( '%1$s %2$s', 'control' === alternativeId ? _x( 'This test protected your site from a change that would have reduced performance. Keeping the control was the right decision!', 'text', 'nelio-ab-testing' ) : _x( 'This test uncovered a better-performing version of your page. Your visitors will benefit from this improvement.', 'text', 'nelio-ab-testing' ), ! args.isWinnerLastApplied && 'control' !== alternativeId ? _x( 'We recommend applying the winning version to all visitors.', 'text', 'nelio-ab-testing' ) : '' ), icon: , primaryAction: args.applyWinner ? { isBusy: false, isDisabled: args.areActionsDisabled, label: sprintf( /* translators: %s: Variant name (as in “Variant X”). */ _x( 'Apply %s', 'command', 'nelio-ab-testing' ), alternativeName ), onClick: args.applyWinner, } : undefined, secondaryAction: args.createFollowUpTest ? { isBusy: args.isCreatingFollowUpTest, isDisabled: args.areActionsDisabled && ! args.isCreatingFollowUpTest, label: _x( 'Create Follow-Up Test', 'command', 'nelio-ab-testing' ), onClick: args.createFollowUpTest, } : undefined, }; } case 'possible-winner': { const alternativeName = args.results.winner.name; return { eyebrow: _x( 'Promising result', 'text', 'nelio-ab-testing' ), title: sprintf( /* translators: %s: Variant name. */ _x( '%s finished ahead', 'text', 'nelio-ab-testing' ), alternativeName ), subtitle: getUpliftHeadline( args.results, 'finished' ), description: _x( 'The result was promising, but there wasn’t enough evidence to declare a clear winner.', 'text', 'nelio-ab-testing' ), icon: , primaryAction: args.createFollowUpTest ? { isBusy: args.isCreatingFollowUpTest, isDisabled: args.areActionsDisabled && ! args.isCreatingFollowUpTest, label: _x( 'Create Follow-Up Test', 'command', 'nelio-ab-testing' ), onClick: args.createFollowUpTest, } : undefined, }; } case 'no-winner': return { eyebrow: _x( 'No significant difference', 'text', 'nelio-ab-testing' ), title: _x( 'No clear winner', 'text', 'nelio-ab-testing' ), subtitle: _x( 'None of the variants clearly outperformed the control', 'text', 'nelio-ab-testing' ), description: _x( 'The test did not identify a clear improvement. Consider trying a different hypothesis.', 'text', 'nelio-ab-testing' ), icon: , secondaryAction: args.createFollowUpTest ? { isBusy: args.isCreatingFollowUpTest, isDisabled: args.areActionsDisabled && ! args.isCreatingFollowUpTest, label: _x( 'Create Follow-Up Test', 'command', 'nelio-ab-testing' ), onClick: args.createFollowUpTest, } : undefined, }; case 'collecting-results': // NOTE. This branch will never run, because this state only occurs while the test is running. // Therefore, there’s no need to do anything (other than returning something so that TS doesn’t complain). return getRunningSummaryContent( args ); } }; type SummaryMetricProps = { label: string; help: string; value: string; detail?: string; }; const SummaryMetric = ( { label, help, value, detail, }: SummaryMetricProps ) => (
{ label }
{ value } { detail && ( { createInterpolateElement( detail.replaceAll( '\n', '
' ), { br:
, } ) }
) }
); type ExperimentStatusMetricProps = { status: ExperimentStatus; startDate: string | false; endDate: string | false; }; const ExperimentStatusMetric = ( { status, startDate, endDate, }: ExperimentStatusMetricProps ) => { const start = toDate( startDate ); const end = status === 'finished' && endDate ? toDate( endDate ) : new Date(); return (
Status
{ status === 'running' ? _x( 'Running', 'text (experiment status)', 'nelio-ab-testing' ) : _x( 'Finished', 'text (experiment status)', 'nelio-ab-testing' ) } { status === 'running' ? sprintf( /* translators: %s: Start date. */ _x( 'Started %s', 'text', 'nelio-ab-testing' ), formatI18nDate( start ) ) : sprintf( /* translators: %s: End date. */ _x( 'Ended %s', 'text', 'nelio-ab-testing' ), formatI18nDate( end ) ) }
{ status === 'running' ? sprintf( /* translators: %s: Day number. */ _x( 'Day %s', 'text', 'nelio-ab-testing' ), getDurationInDays( start, end ) ) : sprintf( /* translators: %s: Amount of time, like “2 months and 3 days.” */ _x( 'Ran for %s', 'text', 'nelio-ab-testing' ), getBeautifulDuration( start, end ) ) }
); }; const ResultsSummarySkeleton = ( { hasValueEstimate, }: { readonly hasValueEstimate: boolean; } ) => (
{ hasValueEstimate && (
) }
); const formatConfidence = ( results: Results ): string => { if ( 'loading' === results.type || 'collecting-results' === results.type ) { return '—'; } if ( 0 === results.confidence ) { return '—'; } return `${ Math.floor( results.confidence ) }%`; }; const getConfidenceLabel = ( results: Results ): Maybe< string > => { if ( 'loading' === results.type || 'collecting-results' === results.type ) { return undefined; } if ( results.confidence >= 95 ) { return _x( 'Very high', 'text', 'nelio-ab-testing' ); } if ( results.confidence >= 80 ) { return _x( 'High', 'text', 'nelio-ab-testing' ); } if ( results.confidence >= 50 ) { return _x( 'Low to medium', 'text', 'nelio-ab-testing' ); } return _x( 'Low', 'text', 'nelio-ab-testing' ); }; const getUpliftHeadline = ( results: Results, status: 'running' | 'finished' ): string | JSX.Element => { if ( 'loading' === results.type || 'collecting-results' === results.type ) { return ''; } if ( 'winner' === results.type || 'possible-winner' === results.type ) { if ( 'control' === results.winner.id ) { return results.alternativeCount > 2 ? _x( 'No variant improved on the control', 'text', 'nelio-ab-testing' ) : _x( 'Variant B didn’t improve on the control', 'text', 'nelio-ab-testing' ); } } if ( 'no-winner' === results.type || ! results.uplift ) { return status === 'running' ? _x( 'Performance is being evaluated', 'text', 'nelio-ab-testing' ) : _x( 'No measurable improvement', 'text', 'nelio-ab-testing' ); } return createInterpolateElement( sprintf( /* translators: %s: Percentage value. */ _x( '%s uplift over the control', 'text', 'nelio-ab-testing' ), `${ results.uplift.toFixed( 1 ) }%` ), { strong: } ); }; function hasEstimatedImpact( results: Results ): boolean { if ( 'winner' !== results.type && 'possible-winner' !== results.type ) { return false; } return !! results.estimatedImpact; } const formatEstimatedImpact = ( results: Results ): string => { if ( 'loading' === results.type || 'collecting-results' === results.type ) { return '—'; } if ( results.estimatedImpact ) { return results.estimatedImpact.value; } if ( results.uplift !== null && Number.isFinite( results.uplift ) ) { return formatSignedPercentage( results.uplift ); } return '—'; }; const getImpactDetail = ( results: Results ): Maybe< string > => { if ( 'loading' === results.type || 'collecting-results' === results.type ) { return undefined; } const parts: string[] = []; if ( results.estimatedImpact ) { const period = _x( 'month', 'text', 'nelio-ab-testing' ); parts.push( `/${ period }` ); if ( results.estimatedImpact.uplift && Number.isFinite( results.estimatedImpact.uplift ) ) { parts.push( `(${ formatSignedPercentage( results.estimatedImpact.uplift ) })` ); } } return parts.join( ' ' ) || undefined; }; const formatSignedPercentage = ( value: number ): string => `${ value > 0 ? '+' : '' }${ value.toFixed( 1 ) }%`; const getDurationInDays = ( startDate: Date, endDate: Date ): number => { const millisecondsPerDay = 1000 * 60 * 60 * 24; return Math.max( 1, Math.ceil( ( endDate.getTime() - startDate.getTime() ) / millisecondsPerDay ) ); }; const getBeautifulDuration = ( startDate: Date, endDate: Date ): string => { const duration = computeDuration( startDate.toISOString(), endDate.toISOString() ); const snippets = [ duration.years, duration.months, duration.days, duration.hours, duration.minutes, ]; const { years, months, days, hours, minutes } = duration; const stringifiedSnippets = [ sprintf( /* translators: %d: Number of years. */ _n( '%d year', '%d years', years, 'nelio-ab-testing' ), years ), sprintf( /* translators: %d: Number of months. */ _n( '%d month', '%d months', months, 'nelio-ab-testing' ), months ), /* translators: %d: Number of days. */ sprintf( _n( '%d day', '%d days', days, 'nelio-ab-testing' ), days ), sprintf( /* translators: %d: Number of hours. */ _n( '%d hour', '%d hours', hours, 'nelio-ab-testing' ), hours ), sprintf( /* translators: %d: Number of minutes. */ _n( '%d minute', '%d minutes', minutes, 'nelio-ab-testing' ), minutes ), ]; for ( let i = 0; i < snippets.length - 1; ++i ) { if ( snippets[ i ] && snippets[ i + 1 ] ) { return joinTimes( stringifiedSnippets[ i ], stringifiedSnippets[ i + 1 ] ); } if ( snippets[ i ] ) { return stringifiedSnippets[ i ] || ''; } } if ( snippets[ snippets.length - 1 ] ) { return stringifiedSnippets[ snippets.length - 1 ] || ''; } return _x( 'less than a minute', 'text', 'nelio-ab-testing' ); }; function joinTimes( a: string | undefined, b: string | undefined ): string { if ( a && b ) { const and = _x( 'and', 'text', 'nelio-ab-testing' ); return `${ a } ${ and } ${ b }`; } return a || b || ''; } const toDate = ( value: string | false ): Date => value ? new Date( value ) : new Date(); const TrophyIcon = () => ( ); const TrendIcon = () => ( ); const NoDifferenceIcon = () => ( ); const CollectingIcon = () => ( ); // ===== // HOOKS // ===== const useResults = () => useSelect( ( select ): Results => { const activeExperiment = select( NAB_DATA ).getPageAttribute( 'editor/activeExperiment' ); const isLoading = ! select( NAB_DATA ).hasFinishedResolution( 'getExperimentResults', [ activeExperiment ] ); if ( ! activeExperiment || isLoading ) { return { type: 'loading' }; } const minSampleSize = select( NAB_DATA ).getPluginSetting( 'minSampleSize' ); const pageViews = select( NAB_DATA ).getPageViews( activeExperiment ) || 0; const experimentStatus = select( NAB_DATA ).getExperimentAttribute( activeExperiment, 'status' ); if ( 'running' === experimentStatus && pageViews < minSampleSize ) { return { type: 'collecting-results' }; } const goals = select( NAB_DATA ).getExperimentAttribute( activeExperiment, 'goals' ) || []; const activeGoalId = select( NAB_DATA ).getPageAttribute( 'editor/activeGoal' ) || ( goals[ 0 ]?.id ?? '' ); const winners = select( NAB_DATA ).getWinnersInExperiment( activeExperiment ); const winner = winners?.[ activeGoalId ]; const minConfidence = select( NAB_DATA ).getPluginSetting( 'minConfidence' ); const confidence = winner?.confidence || 0; const alternatives = select( NAB_DATA ).getExperimentAttribute( activeExperiment, 'alternatives' ) || []; const isUnique = select( NAB_DATA ).areUniqueResultsVisible( activeExperiment ); const resultsOfAlternatives = select( NAB_DATA ).getResultsOfAlternatives( activeExperiment ); const durationInDays = getDurationInDays( toDate( select( NAB_DATA ).getExperimentAttribute( activeExperiment, 'startDate' ) || false ), 'finished' === experimentStatus ? toDate( select( NAB_DATA ).getExperimentAttribute( activeExperiment, 'endDate' ) || false ) : new Date() ); const ecommerce = select( NAB_DATA ).getECommercePlugin( activeExperiment, activeGoalId ); const estimatedImpact = ecommerce ? getEstimatedImpact( activeGoalId, alternatives, resultsOfAlternatives, isUnique, ecommerce, durationInDays ) : undefined; const details: DataDetails = { alternativeCount: alternatives.length, minConfidence, confidence, uplift: 0, estimatedImpact, }; const winningAlternative = alternatives[ winner?.alternative ?? 0 ]; if ( winner === undefined || ! winningAlternative ) { return { type: 'no-winner', ...details, }; } const winnerResults = resultsOfAlternatives?.[ winningAlternative.id ]; const uplift = isUnique ? winnerResults?.uniqueImprovementFactors[ activeGoalId ] || 0 : winnerResults?.improvementFactors[ activeGoalId ] || 0; return { type: confidence < minConfidence ? 'possible-winner' : 'winner', ...details, uplift, winner: { id: winningAlternative.id, name: sprintf( /* translators: %s: Variant letter. */ _x( 'Variant %s', 'text', 'nelio-ab-testing' ), getLetter( winner.alternative ) ), isLastApplied: !! winningAlternative.isLastApplied, }, }; }, [] ); const useExperiment = () => ( { id: useExperimentAttribute( 'id' ), status: 'running' === useExperimentAttribute( 'status' ) ? ( 'running' as const ) : ( 'finished' as const ), startDate: useExperimentAttribute( 'startDate' ) || ( false as const ), endDate: useExperimentAttribute( 'endDate' ) || ( false as const ), hasValueEstimate: useHasValueEstimate(), } ); const useHasValueEstimate = (): boolean => useSelect( ( select ) => { const activeExperiment = select( NAB_DATA ).getPageAttribute( 'editor/activeExperiment' ); if ( ! activeExperiment ) { return false; } const goals = select( NAB_DATA ).getExperimentAttribute( activeExperiment, 'goals' ) || []; const activeGoalId = select( NAB_DATA ).getPageAttribute( 'editor/activeGoal' ) || ( goals[ 0 ]?.id ?? '' ); return !! select( NAB_DATA ).getECommercePlugin( activeExperiment, activeGoalId ); }, [] ); // ======= // HELPERS // ======= function getEstimatedImpact( goalId: GoalId, alternatives: ReadonlyArray< Alternative >, resultsOfAlternatives: Maybe< Record< AlternativeId, AlternativeTrackingData > >, isUnique: boolean, ecommerce: ECommercePlugin, durationInDays: number ): DataDetails[ 'estimatedImpact' ] { if ( ! alternatives.length || ! resultsOfAlternatives ) { return undefined; } const ids = alternatives.map( ( a ) => a.id ); const values = isUnique ? ids.map( ( id ) => resultsOfAlternatives[ id ]?.uniqueValues[ goalId ] || 0 ) : ids.map( ( id ) => resultsOfAlternatives[ id ]?.values[ goalId ] || 0 ); const [ controlValue = 0, ...alternativeValues ] = values; const bestAlternativeValue = alternativeValues.reduce( ( r, x ) => ( x > r ? x : r ), 0 ); if ( ! bestAlternativeValue ) { return undefined; } const impact = bestAlternativeValue - controlValue; if ( impact <= 0 ) { return undefined; } const months = durationInDays / 30; return { value: '+' + getMoneyLabel( Math.floor( impact / months ), ecommerce ), uplift: controlValue ? ( 100 * bestAlternativeValue ) / controlValue : undefined, }; } function getWinner( results: Results ): Maybe< AlternativeId > { if ( 'winner' !== results.type && 'possible-winner' !== results.type ) { return undefined; } return results.winner.id; } const useCanApplyAlternativeWinner = ( experimentId?: ExperimentId, winner?: AlternativeId ): winner is AlternativeId => useSelect( ( select ): boolean => { const canUserApplyAlternatives = select( NAB_DATA ).hasCapability( 'edit_nab_experiments' ); if ( ! canUserApplyAlternatives ) { return false; } const isPublicView = select( NAB_DATA ).getPageAttribute( 'results/isPublicView' ); if ( isPublicView ) { return false; } const experiment = select( NAB_DATA ).getExperiment( experimentId ); if ( ! experiment || ! winner || 'control' === winner ) { return false; } const alternative = experiment.alternatives.find( ( a ) => a.id === winner ); if ( ! alternative || alternative.isLastApplied ) { return false; } const { getExperimentSupport } = select( NAB_EXPERIMENTS ); const alternativeApplication = getExperimentSupport( experiment.type, 'alternativeApplication' ); const supportsAlternativeApplication = isFunction( alternativeApplication ) ? alternativeApplication( experiment.alternatives[ 0 ]?.attributes ) : !! alternativeApplication; return ( experiment.status === 'finished' && !! supportsAlternativeApplication ); }, [ experimentId, winner ] ); const useCanCreateFollowUpTest = () => useSelect( ( select ): boolean => select( NAB_DATA ).hasCapability( 'edit_nab_experiments' ) && !! select( NAB_DATA ).getPluginSetting( 'aiSettings' ), [] ); const useCanStopTest = () => useSelect( ( select ) => select( NAB_DATA ).hasCapability( 'stop_nab_experiments' ), [] ); const POST_OPPORTUNITY_TYPES = [ 'nab/page', 'nab/post', 'nab/custom-post-type', ]; const useOpportunity = ( experimentId: Maybe< ExperimentId > ) => useSelect( ( select ): Maybe< Opportunity > => { const experiment = select( NAB_DATA ).getExperiment( experimentId ); if ( ! isOpportunityFeasible( experiment ) ) { return undefined; } const target = getOpportunityTarget( select, experiment ); if ( ! target ) { return undefined; } return { type: 'recently-finished-experiment', target, score: 100, meta: { experiment: { type: experiment.type, id: experiment.id, name: experiment.name, description: experiment.description, alternatives: experiment.alternatives, goals: experiment.goals, segments: experiment.segments, testedAt: experiment.endDate, }, }, }; }, [ experimentId ] ); function isOpportunityFeasible( experiment: Maybe< Experiment > ): experiment is Experiment { if ( ! experiment ) { return false; } if ( POST_OPPORTUNITY_TYPES.includes( experiment.type ) ) { return true; } return !! getSingleUrlScope( experiment ); } function getOpportunityTarget( select: SelectFunction, experiment: Experiment ): Maybe< Opportunity[ 'target' ] > { if ( POST_OPPORTUNITY_TYPES.includes( experiment.type ) ) { const attrs = experiment.alternatives[ 0 ] .attributes as PostControlAttributes; const post = select( NAB_DATA ).getEntityRecord( attrs.postType, attrs.postId ); if ( ! post ) { return undefined; } return { type: 'post', postId: post.id as PostId, postType: post.type, title: post.title, url: post.link as Url, }; } const url = getSingleUrlScope( experiment ); if ( url ) { return { type: 'url', title: '', url, }; } return undefined; } function getSingleUrlScope( experiment: Experiment ): Maybe< Url > { if ( 'nab/url' === experiment.type ) { return ( experiment.alternatives[ 0 ].attributes as UrlControlAttributes ).url as Url; } const scope = experiment.scope ?? []; if ( ! hasHead( scope ) || scope.length !== 1 ) { return undefined; } const rule = scope[ 0 ].attributes; return rule.type === 'exact' ? ( rule.value as Url ) : undefined; } function getIsWinnerLastApplied( results: Results ): boolean { if ( results.type !== 'winner' && results.type !== 'possible-winner' ) { return false; } return results.winner.isLastApplied; }