DEV Community

Cover image for The Bug That Threw No Errors: How a Repeat-Trial Guard Silently Killed Our Free Trial Funnel
Vicente G. Reyes
Vicente G. Reyes

Posted on

The Bug That Threw No Errors: How a Repeat-Trial Guard Silently Killed Our Free Trial Funnel

This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.

The setup

I'm the lead developer on Rev6.fit, a fitness membership platform built on WordPress + WooCommerce Subscriptions + LearnDash. It runs 49 plugins, five membership families, and a stack of custom code snippets — the kind of production environment where every change has a blast radius.

Rev6's entire top-of-funnel is a 7-day free trial. Every consumer membership — All-Access, On-Demand, Vitality — leads with "$0 today, first charge in 7 days." If the trial disappears, new signups don't slow down. They stop.

And one day, it disappeared. For every prospect. Silently.

The symptom

Logged-out visitors — which is to say, every single potential new customer — landed on membership product pages and saw... a full-price subscription. No "with a 7-day free trial" label. No "$0 today." Just the sticker price.

Existing members saw everything correctly. The client saw everything correctly (logged in, obviously). The site threw zero errors. Sentry — which we run on both the PHP and JS sides — was green. PageSpeed was fine. Checkout worked.

The funnel was just quietly bleeding out.

The catch didn't come from a dashboard. It came from a human: Avie, my primary client contact, noticed the trial messaging was gone from the membership pages and flagged it. That detail matters to the story — with all our monitoring green, the alerting system that actually fired was someone who knows the site well enough to feel when something's off. Every production stack has one of those. They're underrated.

The root cause

Here's the part I love, because it's such a classic shape: the bug was inside a guard written for a different problem.

The site carried a custom snippet whose job was to enforce one free trial per customer — if you'd already consumed your trial, the trial offer was stripped from the subscription for you. Reasonable policy for any trial-led membership business.

The right logic is:

"Has this user already had a trial? If yes, remove the trial."

But the snippet carried an extra condition that effectively turned it into:

"Can I confirm this user deserves a trial? If not, remove the trial."

And for a logged-out visitor, there's no user to check. No account → no trial history → can't confirm eligibility → strip the trial. The guard failed closed against the exact people it should have failed open for: first-time visitors who haven't created an account yet.

A simplified reconstruction of the shape (the real snippet stays private, but this is the logic faithfully):

// The repeat-trial guard — intended behavior
add_filter( 'woocommerce_subscriptions_product_trial_length', function ( $trial_length, $product ) {
    $user_id = get_current_user_id();

    // ❌ The extra condition: guests have no $user_id,
    // so they fell into the "no trial" branch too
    if ( ! $user_id || rev6_user_has_used_trial( $user_id ) ) {
        return 0; // no trial for you
    }

    return $trial_length;
}, 10, 2 );
Enter fullscreen mode Exit fullscreen mode

The subtle trap: on Rev6, guest checkout is disabled — accounts are auto-created at signup. So a guest seeing the trial offer is completely safe; by the time money and trial entitlement are involved, they have an account and the repeat-trial check can do its job. The guest branch in the guard wasn't just wrong — it was protecting against a scenario the platform's own architecture already made impossible.

The fix

The fix was almost anticlimactic — remove the guest condition, keep the repeat-trial protection:

if ( ! $user_id ) {
    return $trial_length; // ✅ guests see the trial — accounts are created at signup anyway
}

if ( rev6_user_has_used_trial( $user_id ) ) {
    return 0; // repeat-trial protection stays intact
}

return $trial_length;
Enter fullscreen mode Exit fullscreen mode

One condition. That's the whole diff. But the verification checklist was the real work:

  • ✅ Logged-out visitor sees "with a 7-day free trial" on every membership product
  • ✅ Checkout shows $0 today / first charge in 7 days
  • ✅ Account auto-creation at signup still works
  • ✅ A user who already consumed a trial still gets no second trial
  • ✅ Verified on staging first, then live The last item on that list matters most: it would have been very easy to "fix" this by deleting the guard entirely — and quietly reopen the repeat-trial loophole the snippet existed to close. Smashing a bug shouldn't resurrect its predecessor.

What Sentry taught me by not catching this

Rev6 runs Sentry on both PHP and JS, and it's caught real production issues for us — an orphaned marketing-automation job erroring every 60 seconds, broken script dependencies on product pages, a Stripe Express Checkout misconfiguration. It's earned its keep.

But this bug was invisible to it, and that's the lesson I'd hand any dev working on revenue-critical flows:

Error monitoring catches code that fails. It can't catch code that succeeds at doing the wrong thing.

The trial guard executed perfectly. No exception, no warning, no log line. It returned 0 exactly as written — the code was correct; the condition was wrong. That's a whole class of bug that lives below the monitoring waterline: pricing logic, discount eligibility, visibility rules, permission gates. The stuff that decides whether money moves.

My takeaways for that class of bug:

  1. Test your money paths logged out. Your default browsing state as a developer (logged in, admin bar on, caches primed) is the state your prospects will never be in. Incognito is a QA tool.
  2. Guards should fail open or closed deliberately. Every early-return in an eligibility check is a policy decision. Write the comment. "Guests: allow, because accounts are created at signup" would have made this bug impossible to write.
  3. Behavioral monitoring complements error monitoring. An alert on "trial signups per day dropped to zero" would have caught this in hours — instead of waiting for a sharp-eyed human to happen across the page. ## The win

The trial is back, the repeat-trial abuse protection still holds, and every first-time visitor to Rev6 now sees the offer that the entire funnel was designed around. No new plugins, no rewrite — one condition removed, with a verification list long enough to trust the change on a live revenue path.

The bugs that throw exceptions are the easy ones. The legendary ones return 0 and walk away clean.


I'm Ice — freelance full-stack dev (Django/React + WordPress/WooCommerce + Shopify). More at vicentereyes.org.

Top comments (0)