A B2B customer wrote in to say our one-time codes were five characters long when the field on his screen only took four, and I spent half a Tuesday in the production database hunting for a bug that turned out to be a column he had not scrolled past. The wider claim is that watching a user does more work than talking to one ever will, and that in B2B SaaS a complaint without a session replay attached is half a complaint at best.
banani · tue · 10:51 am
open the session
app.internal.tools / debug / sup-3041
ElementsConsoleNetworkSessionsStorage
200GET/api/codes?since=24h142 ms · 1.4 kb
{
"codes": [
{ "id": "A7K2P", "otp": "8431"(4 chars) },
{ "id": "M9D4Q", "otp": "2207"(4 chars) },
{ "id": "X1H6N", "otp": "5519"(4 chars) },
{ "id": "B3L8C", "otp": "6184"(4 chars) },
{ "id": "Z5T0G", "otp": "9072"(4 chars) },
],
"meta": { "len": 4 }
}
no bug heretry the sessions tab →
The ticket came in at 10:42 on a Tuesday morning from a senior ops lead at a B2B logistics customer in Mumbai who has been on our platform for almost a year and who, in my private mental model, is the kind of user whose tickets I open first because he is usually right about whatever he is complaining about.
The first thing I did, because I have spent the last six months shipping a lot of code written with AI and have developed a quiet paranoia about exactly this kind of regression, was assume the bug was mine. Some background migration had run, or a config flag had flipped, or the generator function had been silently tweaked by a refactor I had approved on a Friday afternoon when I should have been reading more carefully. I pulled up the OTP service in the codebase. I read the generator. It produced four characters; it had produced four characters since the service was first written; the unit test asserted four characters and was still green. So I went one layer deeper, because of course the unit test would be green if the bug was somewhere the test did not look, and I added frontend validation that would reject any token of any length other than four before it even hit the network. Then I added backend validation that would refuse to issue, log, or accept any OTP that was not exactly four characters long, with an error monitor pointed at the new branch so I would know within seconds if a token of five characters ever appeared in production.
None of it caught anything, because there was nothing to catch.
By about one in the afternoon I had moved on to the production database, which is the place a sensible product owner goes only when he has exhausted every other theory and has decided to do something faintly embarrassing. I queried the OTP table for every row issued in the last seven days, filtered by length, ordered by timestamp, joined against the customer table so I could see whether any of the outliers, five characters long, belonged to my Mumbai user. There were no outliers five characters long. There were not even outliers of four characters with a hidden whitespace at the end. Every single token in the table was exactly four characters of base32, exactly as the generator had been producing them for the last fourteen months. The customer was, in the most literal sense, complaining about a bug that the system had no evidence of having produced.
inspector · breakframe.0 · main
017
018
019
020
021
The strongest signal a senior B2B user can give you, in my experience, is also the easiest one to take at face value and the most expensive one to verify, because by the time you've believed him for three hours you've already shipped two patches and queried a production table you shouldn't have needed to touch.
cite ·me at my desk, the wrong tree
The thing I should have done at 10:43, before any of the validation or the database querying or the slow embarrassment of a wasted Tuesday, was open FullStory and watch the recording. We have a session replay setup on the product I ship, which is an internal B2B platform that a couple of dozen enterprise customers use to run their operations day to day. Every authenticated session in the last ninety days is sitting there with timestamps and a scrubber and a little URL bar at the top showing exactly which page the user was on when whatever they were doing went wrong. I did not open it first. I opened it last, when I had run out of theories and was about ten minutes away from writing the customer an email that began with the words "we cannot reproduce this on our end," which is the sentence that, in B2B SaaS, ends a customer relationship more often than any other sentence in the English language.
10:424G · ▲▲▲
codes· last 24h
id
user
otp
used
A7K2P
asha r.
8431
no
M9D4Q
raj v.
2207
no
X1H6N
neha s.
5519
no
B3L8C
kabir m.
6184
no
Z5T0G
irfan k.
9072
no
user was reading thisotp is here
swipe →one visible
the column on the left is the id. the column on the right is the otp. the user only saw the column on the left.
The table on the codes dashboard has, sitting in the leftmost position, an internal ID column that I added eight months ago for our own support team to reference, and which on a mobile screen in portrait mode is the only column visible without horizontal scrolling. The ID column is five characters of alphanumeric. The OTP column, the one the customer actually wanted, is the second column from the left, and to see it on his phone he would have had to swipe sideways on the table, which the responsive layout does not strongly hint at because the column header bar does not animate or peek the way it does on the marketing site. He was, with complete sincerity and zero error on his part as a user, copying the ID and pasting it where the OTP should go. The "OTP of five characters" he was complaining about was an ID of five characters, which means the thing I had been hunting for six hours was not a bug in the generator but a layout that had quietly trained the customer to read the wrong column for the better part of a day before he gave up and wrote in.
Don Norman makes a version of this point in The Design of Everyday Things, where the through line of the book is that users almost never lie about what they experienced. They lie, accidentally and constantly, about why they experienced it. The customer who reports that the door is broken is reliably telling you that the door did not open when he pushed it; he is unreliably telling you that the door is broken, because the door might be perfectly functional and the problem might be that the handle is shaped like a thing you pull. Jakob Nielsen has spent thirty years saying the neighbouring thing about reading on the web, which is that users do not read pages so much as scan them, picking up about a fifth of the words and inferring the rest from layout and position. Both observations point in the same direction for a PM, which is that the user's report of the symptom is more or less gold, while the user's theory about the cause is a guess made under stress in the thirty seconds before they wrote the ticket, and treating that guess as ground truth is one of the more common ways a product team will burn a working week.
What I think this means in practice, for anyone running a B2B product in 2026, is that the support team's first action on any incoming ticket should be to pull the session replay before anything else gets logged, escalated, or triaged. Not after the customer has been written back to, not after the first reproduction attempt has failed, but before the ticket has even been formally read. We made this a rule on the solutions team about three months ago. The rough internal estimate is that something like half of the tickets coming in under "the product is broken" or "this feature is not working" turned out, on the first viewing of the replay, to be the user operating the product in a way the team had not anticipated and the UI had not strongly steered against. That number was higher than I expected when we put the rule in. It has not gone down since. The bugs that turn out to be real bugs are still real bugs, and the engineering team gets to them faster because they arrive with a replay attached that points at the exact frame the failure happened in, which is also a quiet productivity win nobody costed in advance.
The honest disclaimer here, because the rest of this argument has the shape of a tool recommendation and is not one, is that we happen to use FullStory and I am not paid by FullStory and I have not seriously evaluated the alternatives. Hotjar exists; LogRocket exists; Microsoft Clarity is free; there is a whole quiet category of session replay vendors who will probably do the same job for your team at a different price point and with a different set of tradeoffs around privacy, the cost of scrubbing, and integration weight. The principle is what I am trying to land, not the vendor. A B2B SaaS team that does not have some form of session replay running on the product is operating on customer self-report alone, which is the same epistemic position as a doctor who only ever asks the patient what is wrong and never runs a test. Talking to a user is cheap and useful and absolutely worth doing every week, but watching the user is the part that tells you whether what they told you on the call is what actually happened to them at four in the afternoon on a Tuesday.
The complication, and the reason the rule matters more in B2B than in consumer products, is that B2B users will almost never agree to record themselves on a call. Even our friendliest customers, the ones who will sit on a Zoom for an hour and walk through a workflow with the product team, balk the moment you ask them to share their screen with the recording light on. The most you typically get is a screenshot, often the wrong screen, sometimes annotated with a red arrow that points at the part of the UI they think is the problem, almost never showing the three preceding screens that contain the actual story of how they got there. The replay closes that gap without asking the customer for anything, because the recording was running anyway from the moment they logged in. There is a real privacy conversation to be had about what you record and for how long and which fields you mask, and I have opinions on that, but I'll save them for a different post. The point for this one is that the only honest way to see the path a B2B user took through your product, in a setting where they will not turn the camera on, is to have already been recording.
It was about 3:50pm when the FullStory tab finally loaded and the recording started playing on my second monitor. The scrubber moved past the 02:11 mark, which is where the customer made his third paste attempt, and I watched him close the tab and open the support form, and I felt the specific kind of tired that comes from having spent six hours fixing the wrong bug. The email I wrote to Aarav in Mumbai twenty minutes later opened with an apology, walked him through what the recording had shown, and ended with a small UI change I shipped that evening that pinned the OTP column to the leftmost position on mobile viewports and shaded the ID column a quieter grey. He wrote back the next morning with a single line. Thank you, that was my fault, please ignore yesterday's escalation. It was not, of course, his fault at all, because the fault sat with me for trusting his theory about a broken generator before I had bothered to watch his hands tap the wrong ID, five characters, into the OTP field that only took four, on the wrong half of a phone screen.