Seventeen pull requests in February, fourteen merged. The month contains the clearest example I have of how much work a decision can absorb before producing three lines of change, and it resolves something I left unexplained in October.
Hotjar, for five days
In the October article I wrote about deleting the Hotjar session-recording script as a hotfix, and admitted the issue body was empty so I could not reconstruct why. I called it the only decision in the series I could not read back from a diff.
On 25 February I put it back:
+ <!-- Hotjar Tracking Code for https://3cat.my -->
+ <script>(function (h, o, t, j, a, r) { ...
+ h._hjSettings = { hjid: 5170149, hjsv: 6 }; ... })</script>The same snippet, the same site identifier, byte for byte what had come out four months earlier. The ticket was titled Reactivate hotjar for 5 days and its body was, once again, completely empty.
Three days later I removed it again.
So the answer to October was not a bug or an incident. Session recording was being treated as an instrument: switch it on, watch how people actually behave, switch it off. That is why nobody wrote a justification — to the team it was not a change to the site, it was turning on a light. It also explains something I noticed in November, where a ticket about a confusing sea of red error messages cited Hotjar findings weeks after the script was gone. The recordings were a batch of evidence with a start and an end date.
I would still rather the ticket said what question it was asking. But there is a real discipline in it that I did not see at the time: a tracking script that watches customers is on the page for three days and then is not, rather than sitting there for a year because removing it never became anybody's job.
460 lines, twice, and what shipped instead
On 22 January I opened a redesign of the chat call-to-action — 460 additions, 370 deletions, fourteen files. It was closed on 27 January without merging.
On 3 February I opened it again. I have diffed the two: identical file set, identical line counts, the same 460 and 370. It was closed again.
What shipped, the same day, was this:
1209: Update Chat CTA Text + Font Size +3/-3, 2 filesThree lines. The text and the font size.
I have gone back and forth on how to describe that fairly. The unkind reading is that I wasted two attempts at a redesign nobody wanted. The more accurate one is that the ticket asked to make the chat button work harder, I interpreted that as a redesign, built it, and the answer from review was that the button did not need redesigning — it needed different words in a larger size.
Reopening the identical pull request a fortnight later is the part I would not do again. Nothing had changed about it; I had simply not accepted the first answer. The lesson is not ship less — it is that a closed pull request is information, and re-submitting it unchanged is a way of declining to read it.
Instalments and loans
The largest thing I merged in February was a payments reorganisation: 804 additions, 624 deletions, forty-four files.
We want to highlight installment options available on our checkout page.
1) Reorder the payment methods as shown below
2) When customer selects 'Credit Card Installments', the payment card
opens up: Maybank Credit Card, Public Bank Credit Card, Other Credit CardsCredit card instalments and loan financing had been sitting in the same undifferentiated list as paying outright. Separating them meant a customer choosing to spread payments over twelve months sees that as its own path, with their own bank named on it, rather than as a footnote under a card icon.
Forty-four files for a reordering, which tells you how much of a checkout is presentation. The interesting half is that selecting Maybank or Public Bank has to continue into the ordinary card flow afterwards — so it is a new branch at the top that converges back into an existing one, which is exactly the sort of change that produces bugs in the merge rather than in the new code.
An export that showed images
The bug of the month was reported as Can't extract data from Bagisto: exporting orders from the admin panel did not work. The cause was one missing guard.
- {!! json_decode($record->{$column->getIndex()}, true)['first_click_channel'] !!}
+ {!! json_decode($record->{$column->getIndex()}, true)['first_click_channel'] ?? '-' !!}In December I built first-click attribution, which stores a JSON blob on each order recording where the customer came from. Every order placed before that feature existed has no blob. json_decode returns null, indexing null throws, and one old order in the result set takes down the entire export — not the row, the whole file.
This is the fourth time I have written up this exact mistake. March 2024: indexing categories[0] on a collection that might be empty. July: changing that pattern to first() in another file. December: assuming find() found a variant. February: assuming a decode returned an array. In the December article I wrote that I clearly knew the lesson and clearly did not yet write code with it built in. Two months later, same lesson, same shape, new file.
The rest of that fix was more interesting than the fix. The order export was rendering the same Blade template as the admin datagrid, which meant it was emitting sixty-pixel product thumbnails and image placeholders into a spreadsheet. So the template learned which context it was in:
$isExport = request()->has('export');
@if ($isExport)
@foreach ($order->items as $item)
{{ $item->name }} ({{ $item->qty_ordered }}){{ !$loop->last ? ', ' : '' }}
@endforeach
@else
{{-- Regular display with images for web view --}}Plain text with quantities for the export, thumbnails for the screen. One template serving a web page and a spreadsheet was always going to pick a side eventually.
A related ticket I filed on 25 February — Unable export customers list — is still open. Same feature area, different table, and nobody has got to it.
Removing the feature instead of fixing it
The All Stores page had been revamped with a state selector and a find my nearest store function that used the browser's location to sort the store cards by distance.
It went out, needed a staging fix on 25 February, and on 26 February I merged a hotfix titled 982: Hotfix "Find my nearest store". Fourteen additions, ninety-five deletions. It does not repair the feature. It deletes it — eighty-three lines out of the map script, including the functions that showed the closest store and reordered the cards by proximity.
That is a defensible call and I want to record why rather than dress it up. A geolocation feature that misbehaves on a live store-locator page is worse than no geolocation feature, the shop was open, and the fastest safe state was the one before. Deleting it took minutes; diagnosing why it ordered cards wrongly across nine states and multiple browsers would have taken days with the broken version live throughout.
The thing I would check, if I were reviewing my own work, is whether anybody re-raised it afterwards. A hotfix that removes a feature is only the right answer if somebody writes down that the feature is gone.
A logo for Ramadan
In December I put a Christmas hat on the company's cat logo by overwriting the image file and saving the original beside it under a new name, then reversed it on New Year's Eve. I described it as a pragmatic hack.
On 19 February, for Ramadan:
modified 3cat/resources/images/header/logo.webp
added 3cat/resources/images/header/logo_original.webpThe same two files, the same mechanism, a different festival. A throwaway trick from December had quietly become the way this team does seasonal branding — no config, no date logic, no scheduled job, just a file swap with its own rollback sitting next to it.
I am fond of this for a reason that took me a while to articulate. The version of me from a year earlier would have built the seasonal logo system on the second occurrence, because twice is a pattern and patterns want abstracting. Two hundred lines, a config table, a date comparison, and a thing to maintain every December and every Ramadan. What actually exists is two files and a naming convention that anybody can reverse without reading any code.
The reviews are a config file
Two pull requests I skipped, and together they are the month's second-biggest thread after instalments: 216 additions and 230 deletions across seventeen files, then 54 additions and 48 deletions across fourteen. Both are the social-proof sections — testimonials, customer stories, happy customers.
What they do is mostly presentational. Google's branding arrives properly — a Google logo for the happy-customers block, a yellow star sprite — the round avatar partial is replaced by a testimonial-specific one, and twenty-five lines of homepage CSS come out. The pull request titles are Feedback 1 and Customer reviews, happy customers iteration & cleanup, which is why I could not find them again without going back to the diffs.
The detail worth having is where the reviews live. Eleven of those additions are in config/home.php. The testimonials on the homepage are a hardcoded PHP array, checked into the repository, edited by a developer in a pull request. They are presented with Google's logo beside them.
I want to be careful about what I am claiming. These are real reviews, transcribed from a real Google listing; nothing is fabricated. But the mechanism means the homepage shows a fixed selection chosen once and updated when somebody remembers, under a mark that implies a live feed. In June I would go on to add shuffle() to that same array so the order varies between renders — making a static list look dynamic, which is the same instinct one layer further on.
It belongs in this article because February is the month I wrote about watching three days of session recordings to find out what customers actually did. The reviews section is the opposite motion: the page telling customers what other customers thought, from a file.
What February was
Seventeen pull requests, fourteen merged. Instalment options a customer can find, an order export that no longer breaks on old data, a store locator with one fewer broken feature, and a cat dressed for Ramadan.
The pair I keep thinking about is the 460-line redesign and the three-line change that replaced it. Both were the same ticket. One of them is in production. In December 2023 my instinct when handed an ambiguous request was to build the largest reasonable interpretation of it, and fourteen months later that instinct is still my first one — I just get corrected faster now, mostly by other people, and occasionally by opening the same pull request twice and finally reading the answer.
Revised: this article originally omitted the two pull requests reworking the testimonial and customer-review sections, together about twenty-six per cent of February's changed lines. The section above was added from the original diffs.