
TL;DR
|
Most debugging content shows one feature in isolation. This walkthrough covers the whole path.
The bug: a discount code stopped applying for customers on annual billing plans. It passed every existing test, worked for monthly accounts, and failed only for a subset of real annual customers. Nobody had reproduced it outside production.
Here's how it went from ticket to verified fix.
Key takeaway: start from an isolated copy of production that has the same services, the same configuration, and a copy of production data.
The first step is a branch:
upsun branch debug-branch main --resources-init=parentThis creates a preview environment called debug-branch from main. By default, it clones the parent's data, so the annual-billing accounts where the discount failed are there too. The --resources-init=parent flag gives the new environment the same CPU and memory as production. Leave it out and a development environment starts with minimum resources, which is cheaper but won't match production for performance-sensitive bugs.
Next, open tunnels to the environment's services and export their connection details:
upsun tunnel:open
export PLATFORM_RELATIONSHIPS="$(upsun tunnel:info --encode)"tunnel:open creates SSH tunnels to all of the app's services, database included. The export lets a local copy of the app, or any local tool, connect to them as if they were running on your machine.
Key takeaway: trigger the failure with the same data and services that caused it in production.
With the tunnel open, query one of the failing annual accounts to check its billing state. Then open the preview environment's URL, apply the same discount code to that account, and watch it fail silently, the way customers saw it.
Reproducing is usually the slowest part of a bug like this, because someone has to build test data that behaves like the account that broke. Here, the account that broke is already in the environment.
Key takeaway: write and test the fix against the conditions that caused the bug.
Once the bug reproduces reliably, the rest is normal debugging: read the logs, step through the discount logic, check what changed. You can attach a remote debugger to the running environment over SSH, with upsun xdebug for PHP or an SSH port forward for Node.js, and step through the code against the exact accounts that fail.
The cause: the discount logic checked a monthly billing-cycle field that annual accounts don't populate the same way.
The first fix handled that case. After pushing it to debug-branch and running it against a few more annual accounts in the same environment, a second edge case showed up: multi-year plans, which store the billing cycle in yet another field. The second fix went to the same branch and was checked against both sets of accounts.
Key takeaway: preview environments can keep the data a bug depends on and scrub the data it doesn't.
Debugging against isolated copies of real accounts raises a fair question. Upsun lets you add a sanitization script to your deploy hook that anonymizes personal data on preview environments. You write the script for your schema, for example replacing names, emails, and payment details, and set it to run only on non-production environments. Once it's merged to production, every new preview environment is scrubbed when it's created and first deployed.
For this bug, the script leaves billing state untouched, since that's what the bug depends on. Customer names, emails, and payment details are anonymized.
Set this up before you need it. The sanitize databases docs have working examples for MariaDB with Drupal, PostgreSQL with Django, and PostgreSQL with Symfony.
Key takeaway: validate in the same environment where you diagnosed the bug, so the result tells you something real about production.
Before merging, the fix was validated against the original annual account and the multi-year case, in the same environment where the bug was diagnosed.
That environment runs the same services and configuration as production, with production data minus the fields that were sanitized, and (with --resources-init=parent) the same resources. A fix that passes there has been tested against the conditions that caused the failure, not a mock-up of them. From there:
upsun merge debug-branch
Ready to try this on your own bug? Start a 15-day free trial and clone your first environment.
Does this require different tools at each step?
You still use your usual tools: the CLI, your IDE, your debugger. What stays the same is the environment. Branching, tunneling, debugging, and validating all run against one cloned environment, so you don't rebuild test data or recreate services between steps.
Is the data used for reproduction representative of production?
Yes, for what a bug depends on. The environment is cloned from production, so it has the same services, configuration, and data. If you use a sanitization script, personal data is anonymized, and the rest of the data stays as it is in production. Branch with --resources-init=parent if you also need matching resources.
What happens to the debugging environment once the fix ships?
Merge it, then delete it with upsun environment:delete debug-branch. You can also keep it running if you still need to investigate. It uses resources until you delete or deactivate it.