Skip to content

Stats: add analytics events for the pricing grid - #113372

Merged
kangzj merged 8 commits into
trunkfrom
add/stats-pricing-grid-tracking
Aug 9, 2026
Merged

Stats: add analytics events for the pricing grid#113372
kangzj merged 8 commits into
trunkfrom
add/stats-pricing-grid-tracking

Conversation

@kangzj

@kangzj kangzj commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Part of STATS-366. Follow-up to #113366 (now merged; this branch is rebased onto trunk): adds the analytics events and folds in the review feedback from #113366 (review).

Proposed Changes

  • Add three analytics events to the pricing grid, all through trackStatsAnalyticsEvent (which prefixes jetpack_odyssey_/calypso_ by surface) and all carrying blog_id, following the pattern established in Stats: Add blog_id to upsell impression and analytics events #113364:

    • stats_pricing_grid_view — fires once on mount (the grid renders in place of the dashboard, so a mount is an impression).
    • stats_pricing_grid_paid_cta_clickedGet Paid Stats, fired before navigating to the purchase page.
    • stats_pricing_grid_free_cta_clickedStart for free, fired before the dismissal reveals the dashboard.
  • The purchase page's I will do it later button already records {prefix}_stats_purchase_commercial_skip_button_clicked / {prefix}_stats_purchase_flow_skip_button_clicked — no new event needed there. Checkout attribution is already covered by the from=jetpack-stats-pricing-grid referrer on the paid path.

  • Fold in the Stats: show a pricing grid instead of the dashboard for new sites without a plan #113366 review feedback (second commit):

    • Eligibility fails closed when the purchases fetch errors — an error now reads as "plan state unknown" and falls back to the dashboard, instead of the empty list reading as "no plan" and showing the grid to a site that holds one.
    • The price respects getCurrencyObject's symbolPosition (after-symbol locales like de-DE EUR).
    • The Terms of Service line wraps instead of truncating in longer locales (dropped the jetpack component's nowrap/overflow: hidden).
    • Fixed the site‘s U+2018 apostrophe to U+2019 before translation.
    • Dropped the redundant "Included" labels on the three boolean differentiator rows so the mobile fallback names the feature; desktop rendering is unchanged.
    • (The "free plan left unclaimed" comment is answered on the review — deliberate per the STATS-366 direction.)
  • Await the dismissal before navigating from the purchase page (third commit, from this follow-up comment): the dismiss hook returns the mutation promise and both "I will do it later" handlers await it — bounded by a 2s cap so a hanging request (the mutation retries once after 3s) cannot leave the button looking dead — so in the healthy path the gate's notices GET on the destination route can never read the pre-dismissal state — closing the fire-and-forget race and the empty-cache setQueryData no-op without seeding fabricated notices state.

Why are these changes being made?

The pricing grid replaces the Stats dashboard for eligible new sites, so its funnel (impressions → paid vs free choice → checkout) needs to be measurable to evaluate STATS-366. Events follow the existing stats naming and the blog_id property convention from #113364 so they aggregate alongside the rest of the stats upsell analytics.

Testing Instructions

  1. Check out this branch and build Odyssey against a Jetpack site eligible for the grid (see Stats: show a pricing grid instead of the dashboard for new sites without a plan #113366's testing notes).
  2. Open wp-admin Stats with a Tracks debugger active — jetpack_odyssey_stats_pricing_grid_view fires with blog_id.
  3. Click Get Paid Statsjetpack_odyssey_stats_pricing_grid_paid_cta_clicked fires before the purchase page loads.
  4. Reset the dismissal, click Start for freejetpack_odyssey_stats_pricing_grid_free_cta_clicked fires and the dashboard reveals.

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • For UI changes, have you tested the affected components in dark mode?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

Base automatically changed from add/stats-pricing-grid to trunk August 7, 2026 05:58
@kangzj kangzj self-assigned this Aug 7, 2026
@kangzj
kangzj force-pushed the add/stats-pricing-grid-tracking branch from f07aa1a to fc90308 Compare August 7, 2026 06:09
@kangzj
kangzj requested a review from Nikschavan August 7, 2026 06:36
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Aug 7, 2026
@kangzj
kangzj requested a review from dognose24 August 7, 2026 06:36
@kangzj
kangzj marked this pull request as ready for review August 7, 2026 06:37
dognose24

This comment was marked as outdated.

@dognose24 dognose24 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — events and prefixing are right, and the folded-in feedback checks out (symbolPosition matches the before/after pattern already used in client/dashboard/sites/site-plans/index.tsx; the fail-closed eligibility holds because PURCHASES_SITE_FETCH_FAILED marks the store loaded).

The 2s cap is a solid improvement on trunk's fire-and-forget + 250ms, but it can't close the race on its own: the mutation is retry: 1, retryDelay: 3 * 1000, so a failed first POST always retries past the cap, and with no onSuccess on the mutation, nothing invalidates the pricing_grid: true the gate cached at 2s (staleTime 30s). Someone who lands on the purchase page directly — no notices cache, so the notices && patch is a no-op — and hits a failed POST gets shown the grid right after declining a plan.

Adding an onSuccess that invalidates the notices query would close it for good, and for every other notice using that mutation. Inherited from #113366, so fine as a follow-up — after which the 2s race stops being load-bearing and could go away, along with the duplicated Promise.race block in the two skip handlers.

One small thing: the skip button has no busy state, so it can now sit unresponsive for up to 2s where it used to be 250ms.

@Nikschavan Nikschavan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR, this tested well. I have added some suggestions inline

Comment thread client/my-sites/stats/pricing-grid/hooks/use-dismiss-pricing-grid.ts Outdated
Comment thread client/my-sites/stats/stats-purchase/stats-purchase-single-item.tsx
@kangzj

kangzj commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Feedback addressed:

  • Dismissal raceuseNoticeVisibilityMutation now invalidates the notices query in a mutation-level onSuccess (ab8a9b1), so a gate that fetched pre-dismissal state self-corrects once the POST lands — including the direct-landing path where the cache patch is a no-op, and regardless of when the retry fires. This benefits every notice using the mutation. With that in place the awaited navigation was no longer load-bearing, so the Promise.race blocks are gone and both skip handlers are back to the plain dismiss-and-navigate shape — which also resolves the busy-state and duplication notes. The rejection swallowing moved into useDismissPricingGrid, so call sites are a plain call.
  • Skip event attribution — both skip events now carry from (8a248a7).
  • getPurchasesError breadth — comment now says the field is shared across all purchases actions and stays fail-closed either way (c3b9c47).
  • Tests — added pricing-grid/hooks/test/use-eligibility.test.ts covering eligibility, the launch-date cutoff, both created_at formats, the fail-closed error path, and the loading scoping (c3b9c47).

Comment thread client/my-sites/stats/pricing-grid/pricing-grid.tsx
Comment thread client/my-sites/stats/pricing-grid/pricing-grid.tsx Outdated
Comment thread client/my-sites/stats/pricing-grid/pricing-grid.tsx Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Looks like one of the E2E tests has failed.

You can fix them following these steps:

  1. Check out this branch locally:
    gh pr checkout 113372
  2. Start Claude Code in the repo:
    claude
  3. Run the /fix-e2e-tests skill, passing this PR number:
    /fix-e2e-tests 113372
    

@dognose24 dognose24 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the three follow-up commits — both points from my earlier review are addressed, and the invalidation is done more carefully than I suggested: putting onSuccess at the mutation level rather than on the mutate() call is right, since query-core only runs the per-call callbacks while the calling component is still mounted, and the skip handlers navigate away before the retry lands.

ToS

Before After
截圖 2026-08-07 下午3 47 03 截圖 2026-08-07 晚上9 33 17

Feature list

Before After
截圖 2026-08-07 下午3 47 53 截圖 2026-08-07 晚上9 33 24

Events

截圖 2026-08-07 晚上9 34 54 截圖 2026-08-07 晚上9 35 06 截圖 2026-08-07 晚上9 35 27

Dropping the Promise.race follows from that — with self-correction in place the cap wasn't buying correctness any more, and the button is back to its original 250ms. Worth watching in testing: the gate can now fetch pre-dismissal state and briefly show the grid before the invalidation clears it, so a short flash after I will do it later is expected rather than a regression.

from on the skip events comes off the typed prop, and the eligibility tests cover the launch-date boundary, the plan check and the fail-closed error path.

kangzj added 8 commits August 10, 2026 08:48
stats_pricing_grid_view fires on mount, stats_pricing_grid_paid_cta_clicked
and stats_pricing_grid_free_cta_clicked on the two CTAs — all through
trackStatsAnalyticsEvent (which prefixes jetpack_odyssey_/calypso_ by
surface) and all carrying blog_id, following the pattern from #113364.
The purchase page's skip button already has its own event.
- Eligibility fails closed when the purchases fetch errors: FETCH_FAILED marks
  the store loaded with an empty list, which read as 'no plan' and would have
  shown the grid to a site that holds one. An error now means 'plan state
  unknown' and falls back to the dashboard.
- The price respects getCurrencyObject's symbolPosition, so locales that put
  the currency symbol after the amount (e.g. de-DE EUR) render correctly.
- The Terms of Service line wraps instead of truncating: dropped the jetpack
  component's nowrap/overflow-hidden, which cut off the disclosure in locales
  that run longer than English.
- Fixed the apostrophe in 'sync your site's data' (was U+2018, the opening
  quote) before the string goes out for translation.
- Dropped the redundant 'Included' labels on the three boolean differentiator
  rows so the mobile fallback names the feature instead of showing a bare
  'Included' with nothing identifying the row. Desktop rendering is unchanged
  (the default label is already 'Included', bolded via strong).
…rchase page

The dismissal was fire-and-forget with a fixed 250 ms before navigating, so on
a slow request the gate mounted on the destination route could issue its
notices GET before the POST landed and re-render the grid after 'I will do it
later'. The cache patch couldn't cover this either: the raw notices entry is
absent when the purchase page was reached directly (nothing on it queries
notices) or when the dwell outlived the cache's gcTime, and setQueryData with
an updater returning undefined is a no-op — while seeding a fabricated notices
object would feed the other notice consumers made-up server state.

The dismiss hook now returns the mutation promise (mutateAsync); both skip
handlers await it before navigating, still navigating if the request fails.
The grid's free CTA stays same-route and unawaited, just absorbing the
rejection.
…r stalls

The dismissal mutation retries once after 3s, so awaiting it unbounded could
leave 'I will do it later' visually dead for many seconds on a flaky network.
Race it against a 2s cap: the healthy path (a fast POST) stays fully ordered
before navigation, while past the cap the request continues in flight and
navigation proceeds.
The mutation retries once after 3s, so any capped await on the dismissal
was arithmetically guaranteed to lose to a failed first POST, leaving the
stale pricing_grid state cached for up to 30s. Invalidating the notices
query from the mutation (mutation-level, since per-call callbacks are
skipped once the caller unmounts on navigation) self-corrects whenever
the POST lands, for every notice using this mutation. That makes the
awaited navigation redundant, so the skip handlers return to the plain
dismiss-and-navigate shape and the rejection handling moves into the
dismiss hook.
The dismissal now fires regardless of referrer, so without `from` a skip
that came from a module upsell is indistinguishable from one off the
pricing grid.
Also widen the getPurchasesError comment: the field is shared across all
purchases actions, not just the site fetch — fail-closed either way.
@kangzj
kangzj force-pushed the add/stats-pricing-grid-tracking branch from f79baaf to 20b7fb2 Compare August 9, 2026 20:49
@kangzj
kangzj merged commit 1f4521e into trunk Aug 9, 2026
11 of 12 checks passed
@kangzj
kangzj deleted the add/stats-pricing-grid-tracking branch August 9, 2026 21:06
@github-actions github-actions Bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants