Using Testers

Every operation has a command

All of them are pithy testers, against an inbox provisioning stood up.

pithy testers create closed-test --store-url https://play.google.com/apps/testing/com.example.app
pithy testers invite closed-test --email ada@example.com
pithy testers run                    # ask them, chase them, record the day
pithy testers status closed-test

Non-interactive, and all of them take --json.

Cohort:    closed-test
Estimated: 12 of 12 opted in. Day 9 of 14.
Observed:  9 active. 2 quiet 8+ days. 1 never signed in.
Forecast:  71% (55–84%). moderate confidence.
Trend:     Declining. Two testers went dark this week.

This is Pithy's estimate from your own invite records.
Google's count is authoritative and no API exposes it.

You get those last two lines on every status output, --json included — why the count is an estimate is why they are not behind a flag.

Two emails, and why it cannot be one

The order is forced by the store, not chosen.

The store’s opt-in page only works once that address is already on the tester list — and adding an address to a Play email list is precisely what the Play Developer API cannot do, so it is a manual step in the Console.

Send the store link before that step completes and the tester gets App not available, which reads to them as a broken app.

So:

  1. “Will you help test?” — records their answer, and confirms the address is deliverable and correct before it goes on a Google-visible list.
  2. You add the confirmed addresses to Play Console.
  3. “You’re on the list — here’s the link” — carrying the store’s own opt-in page.

That also keeps the count honest: saying yes is consent, not enrollment, and accepted and opted_in are separate states for exactly that reason.

Configure it

testers({
  baseUrl: "https://api.example.com",
  cohortDefaults: {
    targetSize: 12,   // Play's floor
    windowDays: 14,   // Play's window
    maxRosterSize: 100,
  },
  nudges: { cooldownHours: 72 },
})

Set baseUrl before you send anything — an email cannot carry a relative link.

Add the email capability to the same Worker if it is not already there. Invitations and nudges are enqueued through it, so they inherit its retries, suppression list and bounce handling rather than opening a second delivery path with none of them.

Three kinds of nudge, with default copy

Confirm your opt-in. You have not opened the app recently. The window is closing.

Defaults are not a courtesy — without them the capability would only work if you bought a dashboard.

The join links expire; the opt-out link never does.

optInLinkTtlDays bounds how long an invitation stays good, measured from the last time it was sent.

Withdrawing is exempt, and deliberately so. Nothing bounds outreach by that same number, so an opted-in tester is still nudged on day 40 while their last invitation sits on day 0. Under one shared lifetime their unsubscribe link would have died on day 30 with the mail carrying it still arriving.

On all four kinds.

Transactional mail normally carries none — but this capability asks one person for something repeatedly over a fortnight, and a tester being chased must be able to stop it.

The link opens a page that asks, and a button on it withdraws them. Two steps rather than one, because mail clients prefetch links and scanners follow them — and withdrawing rotates their token, so it is irreversible without a fresh invitation.

The rotation is also what kills every link already sitting in their inbox.

Chasing stops after three unanswered messages

The cooldown bounds how often somebody is mailed. This bounds how many times.

The counter resets when they answer — accepting the invitation, or confirming the opt-in. The two replies this asks for.

Opening the app does not clear it, because activity is a separate signal with its own penalties, and a tester who installs but never replies is exactly who the cap is for.

Once somebody has opted in they have no reply left to give, so three unanswered reminders stop their chasing for good. They stay on the roster and stay visible in the roster listing; they simply stop being mailed.

The cooldown is enforced server-side on every path

Including the daily pass and resend.

A nudge trigger with no guard is a button that mails the same twelve people repeatedly — and the fastest way to lose a cohort is to become the reason they muted you.

One snapshot per cohort per UTC day

A daily Workflow writes it, and that table is what makes the trend chartable.

The opt-in figures could always be replayed. Activity could not. Sessions expire and rotate, and a tester who was quiet on the 9th but active on the 11th leaves no trace on the 12th that they were ever quiet.

The early-warning signal has to be captured on the day or it is gone.

Each snapshot carries its own precomputed deltas and trend sentence, so a summary card renders from one row — no client-side series arithmetic, and therefore no way for the card and the chart to disagree.

ESC