diff --git a/projects/packages/premium-analytics/changelog/uni-757-readiness-question b/projects/packages/premium-analytics/changelog/uni-757-readiness-question
new file mode 100644
index 00000000000..d2ca96aeae3
--- /dev/null
+++ b/projects/packages/premium-analytics/changelog/uni-757-readiness-question
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Dashboard: ask whether the new Traffic tab is ready to replace the old one, instead of how it compares with the old one.
diff --git a/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-fields.tsx b/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-fields.tsx
index ff5b52ad2e3..6b897370fba 100644
--- a/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-fields.tsx
+++ b/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-fields.tsx
@@ -7,8 +7,11 @@ import { RadioControl } from '@wordpress/components';
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+/** How ready the reader thinks the new tab is. The value is what reaches Tracks. */
+export type StatsFeedbackReadiness = 'ready' | 'almost' | 'not_yet';
+
// Tracks drops an event whose properties are oversized, so a pasted essay would
-// cost us the rating too.
+// cost us the answer too.
const COMMENT_MAX_LENGTH = 1000;
/**
@@ -26,13 +29,51 @@ function ratingOptions() {
];
}
+/**
+ * The readiness answers, readiest first. `value` is what reaches Tracks.
+ *
+ * @return The options, in the order they are offered.
+ */
+function readinessOptions(): { value: StatsFeedbackReadiness; label: string }[] {
+ return [
+ {
+ value: 'ready',
+ label: __( "Yes, I'd be happy to switch now", 'jetpack-premium-analytics-pkg' ),
+ },
+ {
+ value: 'almost',
+ label: __( 'Almost — there are a few things missing', 'jetpack-premium-analytics-pkg' ),
+ },
+ { value: 'not_yet', label: __( 'Not yet', 'jetpack-premium-analytics-pkg' ) },
+ ];
+}
+
+/**
+ * The reader's readiness answer as Happiness reads it in the ticket body.
+ *
+ * Untranslated on purpose: this is triage copy, like the product name, not reader copy.
+ *
+ * Bracketed, not newline separated: the endpoint runs the message through
+ * `sanitize_text_field`, which collapses every run of whitespace to one space.
+ *
+ * @param readiness - The answer the reader picked.
+ * @return The question and its answer as one line.
+ */
+export function readinessSummary( readiness: StatsFeedbackReadiness ) {
+ const answers: Record< StatsFeedbackReadiness, string > = {
+ ready: 'Yes, ready to switch now',
+ almost: 'Almost, a few things missing',
+ not_yet: 'Not yet',
+ };
+
+ return `[Ready to replace the old Traffic tab? ${ answers[ readiness ] }]`;
+}
+
/**
* The question above a control whose own label is hidden.
*
- * The control keeps the question as its accessible name, so this copy is for the
- * eye only and stays out of the accessibility tree rather than being read twice.
- * The label slot itself is an 11px uppercase caption, which a sentence-long
- * question reads badly as.
+ * Kept out of the accessibility tree so the control's accessible name is not read twice; the
+ * label slot itself is an 11px uppercase caption, which a sentence-long question reads badly as.
*
* @param {object} props - Component props.
* @param {string} props.children - The question.
@@ -42,7 +83,37 @@ function Question( { children }: { children: string } ) {
return { children };
}
-type FeedbackFieldsProps = {
+type CommentFieldProps = {
+ question: string;
+ comment: string;
+ onCommentChange: ( comment: string ) => void;
+};
+
+/**
+ * The open question every feedback surface ends on.
+ *
+ * @param {CommentFieldProps} props - Component props.
+ * @param {string} props.question - The question above the box.
+ * @param {string} props.comment - The comment as typed.
+ * @param {Function} props.onCommentChange - Called with the comment as typed.
+ * @return The field.
+ */
+function CommentField( { question, comment, onCommentChange }: CommentFieldProps ) {
+ return (
+
+ { question }
+
+
+ );
+}
+
+type ComparisonFieldsProps = {
rating: StatsFeedbackRating | undefined;
onRatingChange: ( rating: StatsFeedbackRating ) => void;
comment: string;
@@ -51,24 +122,23 @@ type FeedbackFieldsProps = {
};
/**
- * The comparison scale and the comment box every feedback surface shares; each
- * surface asks its own open question under the scale.
+ * The comparison scale, above an open question the surface chooses.
*
- * @param {FeedbackFieldsProps} props - Component props.
- * @param {number|undefined} props.rating - The score picked, if any.
- * @param {Function} props.onRatingChange - Called with the score picked.
- * @param {string} props.comment - The comment as typed.
- * @param {Function} props.onCommentChange - Called with the comment as typed.
- * @param {string} props.commentQuestion - The question above the comment box.
+ * @param {ComparisonFieldsProps} props - Component props.
+ * @param {number|undefined} props.rating - The score picked, if any.
+ * @param {Function} props.onRatingChange - Called with the score picked.
+ * @param {string} props.comment - The comment as typed.
+ * @param {Function} props.onCommentChange - Called with the comment as typed.
+ * @param {string} props.commentQuestion - The question above the comment box.
* @return The two fields.
*/
-export function FeedbackFields( {
+export function ComparisonFields( {
rating,
onRatingChange,
comment,
onCommentChange,
commentQuestion,
-}: FeedbackFieldsProps ) {
+}: ComparisonFieldsProps ) {
const comparisonQuestion = __(
'Compared with the existing Traffic tab in Stats, the new Traffic tab is:',
'jetpack-premium-analytics-pkg'
@@ -92,16 +162,72 @@ export function FeedbackFields( {
/>
+
+ >
+ );
+}
+
+type ReadinessFieldsProps = {
+ readiness: StatsFeedbackReadiness | undefined;
+ onReadinessChange: ( readiness: StatsFeedbackReadiness ) => void;
+ comment: string;
+ onCommentChange: ( comment: string ) => void;
+};
+
+/**
+ * The readiness question and the open question that follows it.
+ *
+ * @param {ReadinessFieldsProps} props - Component props.
+ * @param {string|undefined} props.readiness - The answer picked, if any.
+ * @param {Function} props.onReadinessChange - Called with the answer picked.
+ * @param {string} props.comment - The comment as typed.
+ * @param {Function} props.onCommentChange - Called with the comment as typed.
+ * @return The two fields.
+ */
+export function ReadinessFields( {
+ readiness,
+ onReadinessChange,
+ comment,
+ onCommentChange,
+}: ReadinessFieldsProps ) {
+ const readinessQuestion = __(
+ 'Is the new Traffic tab ready to replace the old one?',
+ 'jetpack-premium-analytics-pkg'
+ );
+
+ // "What's missing?" reads oddly after "Yes", where nothing is missing by definition.
+ const commentQuestion =
+ readiness === 'ready'
+ ? __( "Any other feedback you'd like to share?", 'jetpack-premium-analytics-pkg' )
+ : __( "What's missing?", 'jetpack-premium-analytics-pkg' );
+
+ const selectReadiness = useCallback(
+ ( value: string ) => onReadinessChange( value as StatsFeedbackReadiness ),
+ [ onReadinessChange ]
+ );
+
+ return (
+ <>
- { commentQuestion }
- { readinessQuestion }
+
+
+
>
);
}
diff --git a/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-modal.tsx b/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-modal.tsx
index 498c0de4fb1..0427febc580 100644
--- a/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-modal.tsx
+++ b/projects/packages/premium-analytics/routes/dashboard/components/feedback/feedback-modal.tsx
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { submitStatsUserFeedback, type StatsFeedbackRating } from '@jetpack-premium-analytics/data';
+import { submitStatsUserFeedback } from '@jetpack-premium-analytics/data';
import { Button, Dialog, Notice, Stack } from '@jetpack-premium-analytics/externals';
import { useCallback, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
@@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import { useTrackEvent } from '../../hooks/use-track-event';
-import { FeedbackFields } from './feedback-fields';
+import { ReadinessFields, readinessSummary, type StatsFeedbackReadiness } from './feedback-fields';
// Reaches Happiness as the subject line of the feedback email ("Feedback received
// from …"), so it has to name the surface without any further context.
@@ -20,7 +20,7 @@ type FeedbackModalProps = {
};
/**
- * Comparison rating with an optional comment. Both reach Tracks as one event; a non-empty
+ * Readiness answer with an optional comment. Both reach Tracks as one event; a non-empty
* comment also goes to the Stats feedback endpoint.
*
* @param {FeedbackModalProps} props - Component props.
@@ -29,15 +29,10 @@ type FeedbackModalProps = {
*/
export function FeedbackModal( { onClose }: FeedbackModalProps ) {
const trackEvent = useTrackEvent();
- const [ rating, setRating ] = useState< StatsFeedbackRating >();
+ const [ readiness, setReadiness ] = useState< StatsFeedbackReadiness >();
const [ comment, setComment ] = useState( '' );
const [ hasSubmitted, setHasSubmitted ] = useState( false );
- const blockerQuestion = __(
- "What's the one thing we'd need to fix before this replaces the old Stats?",
- 'jetpack-premium-analytics-pkg'
- );
-
const handleOpenChange = useCallback(
( isOpen: boolean ) => {
if ( ! isOpen ) {
@@ -48,28 +43,30 @@ export function FeedbackModal( { onClose }: FeedbackModalProps ) {
);
const submit = useCallback( () => {
- if ( rating === undefined ) {
+ if ( readiness === undefined ) {
return;
}
const message = comment.trim();
- trackEvent( 'jetpack_premium_analytics_feedback_submit', { rating, comment: message } );
+ trackEvent( 'jetpack_premium_analytics_feedback_submit', { readiness, comment: message } );
// Second channel, deliberately not awaited: Tracks is a pixel and ad blockers drop it
// silently, so the message also goes to Happiness where delivery is not the reader's
- // browser's decision. A rating alone would only open an empty ticket.
+ // browser's decision. An answer alone would only open an empty ticket.
if ( message ) {
- submitStatsUserFeedback( { rating, comment: message, productName: PRODUCT_NAME } ).catch(
- () => {
- // The reader has already been thanked and Tracks may well have the submission;
- // a second, contradictory message would cost more than the lost email.
- }
- );
+ // The endpoint has no readiness field, so the answer rides along in the message.
+ submitStatsUserFeedback( {
+ comment: `${ readinessSummary( readiness ) } ${ message }`,
+ productName: PRODUCT_NAME,
+ } ).catch( () => {
+ // The reader has already been thanked and Tracks may well have the submission;
+ // a second, contradictory message would cost more than the lost email.
+ } );
}
setHasSubmitted( true );
- }, [ comment, rating, trackEvent ] );
+ }, [ comment, readiness, trackEvent ] );
return (
@@ -110,12 +107,11 @@ export function FeedbackModal( { onClose }: FeedbackModalProps ) {
<>
-
@@ -127,7 +123,7 @@ export function FeedbackModal( { onClose }: FeedbackModalProps ) {