VIN input shows misleading "I/O/Q not allowed" error for masked VINs (asterisks) — causes rage-clicks #89

Open
opened 2026-06-03 20:08:54 +03:00 by root · 1 comment
Owner

TL;DR (TR): Kullanıcı maskeli VIN (VF7S1VJZ*********) yapıştırdığında, string tam 17 karakter olduğu için "Şase Çöz" butonu aktif oluyor; submit'te regex yıldızları reddedip "17 karakter olmalı, I, O, Q harfleri kullanılamaz" diyor — oysa girdi zaten 17 karakter ve asıl sorun (maskeleme/yıldız) hiç söylenmiyor. Kullanıcı ne yapacağını anlamayıp rage-click yapıyor. Küçük, frontend-lokal bir düzeltme.

Problem

Users frequently paste a privacy-masked VIN — e.g. VF7S1VJZ*********, TMAJB81A********* — copied from a marketplace listing, a screenshot, or a document that hides part of the chassis number. On /dashboard/search:

  1. The masked string is exactly 17 chars, so the progress bar fills and the "Şase Çöz" button becomes enabled.
  2. On submit, the VIN regex rejects the * characters and the user is shown:

    "Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz."

  3. This message is wrong on two counts: the input already is 17 characters, and the real problem (the asterisks / masking) is never mentioned. There is no inline (pre-submit) feedback, no red border, and no hint that the VIN looks masked.

Result: the user re-reads the error, retries the same masked VIN, and rage-clicks because the message does not match what they typed.

Evidence (file:line — apps/web)

  • apps/web/src/routes/dashboard/search.tsx:35-51sanitizeVin() only auto-corrects [IOQioq]. Asterisks and other non-alphanumerics pass through unchanged; there is no inline validation of the remaining characters before submit.
  • apps/web/src/routes/dashboard/search.tsx:552maxLength={17}, and :598 — submit is disabled only when vin.length !== 17. A 17-char masked VIN keeps the button enabled.
  • apps/web/src/routes/dashboard/search.tsx:18VIN_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/.
  • apps/web/src/routes/dashboard/search.tsx:318-326 — on submit, isValidVin() rejects *, fires search_input_validation_failed { error_type: "invalid_chars" }, and sets error = t("search.errorInvalidVin").
  • apps/web/src/messages/tr.json:677 (and en.json:677) — the only invalid-VIN message mentions length + I/O/Q, never masked/invalid characters.
  • Backend mirrors the same generic message: apps/api/src/common/pipes/vin-validation.pipe.ts:13-16 and apps/api/src/vehicles/vehicles.service.ts:92-94. (Frontend blocks first, so the backend isn't reached in this masked-VIN case — but it has the same UX gap if hit directly.)

Data

In posthog_events, all 19 search_input_validation_failed events are error_type=invalid_chars with input_length=17. Because sanitizeVin already strips I/O/Q on input, a 17-char invalid_chars failure can only be caused by characters outside [A-HJ-NPR-Z0-9IOQ] — i.e. asterisks/masks/punctuation — which is the masked-VIN signature. The VIN value is intentionally not captured on this event (search.tsx:319-323), so the literal masked strings live in session recordings.

Reproduction

  1. Go to /dashboard/search.
  2. Paste VF7S1VJZ********* (8 chars + 9 asterisks = 17).
  3. The progress bar fills and the "Şase Çöz" button enables.
  4. Click it → error reads "17 karakter olmalı, I, O, Q harfleri kullanılamaz." despite the string being 17 chars and containing no I/O/Q.

Proposed fix (frontend, small, localized to search.tsx)

  1. Detect masked/illegal characters in handleVinChange / sanitizeVin: any char that is not VIN-legal and not an auto-correctable I/O/Q — specifically *, spaces, dashes, punctuation.
  2. Add a mask-aware error string, e.g.:
    • search.errorMaskedVin (tr): "VIN numarası maskelenmiş görünüyor (yıldız/eksik karakter içeriyor). Lütfen tam 17 karakterlik şase numarasını girin."
    • search.errorMaskedVin (en): "This VIN looks masked (it contains * or hidden characters). Please enter the full 17-character VIN."
      Branch to it in handleSearch when the rejected input contains * or other non-alphanumerics, instead of always using errorInvalidVin.
  3. Live feedback before click: show an inline red border + helper text as soon as the field contains masked/illegal characters; optionally keep the submit button disabled in that state so the user gets feedback before clicking.
  4. (Defensive, backend) Align VinValidationPipe message so an asterisk-containing VIN returns a mask-specific 400 reason too, for API parity.

Acceptance criteria

  • Pasting a 17-char masked VIN containing * shows a mask-specific message (not the I/O/Q message).
  • The masked/illegal-char condition produces inline feedback before submit (red border + helper text).
  • A genuinely valid 17-char VIN still decodes with no new friction (no false positives on legal chars).
  • An invalid VIN that is the wrong length still shows the length message; an invalid VIN with I/O/Q still auto-corrects as today.
  • New i18n keys exist in both tr.json and en.json.
  • (Optional) Backend VinValidationPipe returns a mask-specific reason for asterisk VINs.

Verification checklist (confirm "fixed")

  • Playwright repro (pre-fix): script that pastes VF7S1VJZ********* and asserts the OLD misleading message appears (captured before merge as the failing baseline).
  • Playwright (post-fix): same script now asserts the new mask-aware message + red border, and that the submit either stays disabled or surfaces the correct message.
  • Regression cases pass: (a) valid VIN NMTKH4BX60R107701 decodes; (b) vin with lowercase i/o/q auto-corrects; (c) 16-char input shows length error.
  • Data signal: after deploy, search_input_validation_failed events with error_type=invalid_chars & input_length=17 (the masked-VIN signature) trend down; ideally add a new error_type=masked_vin to make this directly measurable.
  • No new rage-clicks on /dashboard/search for sessions that hit the masked path (check session recordings / $rageclick).
  • Insight cmpe55g5 in the panel does not recur (pipeline will auto-flag as regressed if it does).

Notes / scope

  • The panel commit 9a479f9 ("distinguish client-side VIN validation rejects from provider failures") is in the observability/panel repo, not this product — it changes how insights are classified, not product VIN handling. No product commit addresses masked VINs.
  • Out of scope: server-side VIN provider behavior; the separate "OEM part code typed into the VIN box" UX gap (tracked separately / low signal).

Filed from Süper Panel insight triage (2026-06-03). Covers insight(s): cmpe55g570. Canonical: cmpe55g57000mflajghytzgb6.

> **TL;DR (TR):** Kullanıcı maskeli VIN (`VF7S1VJZ*********`) yapıştırdığında, string tam 17 karakter olduğu için "Şase Çöz" butonu aktif oluyor; submit'te regex yıldızları reddedip **"17 karakter olmalı, I, O, Q harfleri kullanılamaz"** diyor — oysa girdi zaten 17 karakter ve asıl sorun (maskeleme/yıldız) hiç söylenmiyor. Kullanıcı ne yapacağını anlamayıp rage-click yapıyor. Küçük, frontend-lokal bir düzeltme. ## Problem Users frequently paste a **privacy-masked VIN** — e.g. `VF7S1VJZ*********`, `TMAJB81A*********` — copied from a marketplace listing, a screenshot, or a document that hides part of the chassis number. On `/dashboard/search`: 1. The masked string is exactly **17 chars**, so the progress bar fills and the **"Şase Çöz"** button becomes **enabled**. 2. On submit, the VIN regex rejects the `*` characters and the user is shown: > **"Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz."** 3. This message is **wrong on two counts**: the input already *is* 17 characters, and the real problem (the asterisks / masking) is never mentioned. There is no inline (pre-submit) feedback, no red border, and no hint that the VIN looks masked. Result: the user re-reads the error, retries the same masked VIN, and **rage-clicks** because the message does not match what they typed. ## Evidence (file:line — `apps/web`) - `apps/web/src/routes/dashboard/search.tsx:35-51` — `sanitizeVin()` only auto-corrects `[IOQioq]`. **Asterisks and other non-alphanumerics pass through unchanged**; there is no inline validation of the remaining characters before submit. - `apps/web/src/routes/dashboard/search.tsx:552` — `maxLength={17}`, and `:598` — submit is disabled only when `vin.length !== 17`. A 17-char masked VIN keeps the button **enabled**. - `apps/web/src/routes/dashboard/search.tsx:18` — `VIN_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/`. - `apps/web/src/routes/dashboard/search.tsx:318-326` — on submit, `isValidVin()` rejects `*`, fires `search_input_validation_failed { error_type: "invalid_chars" }`, and sets `error = t("search.errorInvalidVin")`. - `apps/web/src/messages/tr.json:677` (and `en.json:677`) — the **only** invalid-VIN message mentions length + I/O/Q, never masked/invalid characters. - Backend mirrors the same generic message: `apps/api/src/common/pipes/vin-validation.pipe.ts:13-16` and `apps/api/src/vehicles/vehicles.service.ts:92-94`. (Frontend blocks first, so the backend isn't reached in this masked-VIN case — but it has the same UX gap if hit directly.) ## Data In `posthog_events`, **all 19** `search_input_validation_failed` events are `error_type=invalid_chars` with `input_length=17`. Because `sanitizeVin` already strips I/O/Q on input, a 17-char `invalid_chars` failure can **only** be caused by characters outside `[A-HJ-NPR-Z0-9IOQ]` — i.e. asterisks/masks/punctuation — which is the masked-VIN signature. The VIN value is intentionally not captured on this event (`search.tsx:319-323`), so the literal masked strings live in session recordings. ## Reproduction 1. Go to `/dashboard/search`. 2. Paste `VF7S1VJZ*********` (8 chars + 9 asterisks = 17). 3. The progress bar fills and the **"Şase Çöz"** button enables. 4. Click it → error reads *"17 karakter olmalı, I, O, Q harfleri kullanılamaz."* despite the string being 17 chars and containing no I/O/Q. ## Proposed fix (frontend, small, localized to `search.tsx`) 1. **Detect masked/illegal characters** in `handleVinChange` / `sanitizeVin`: any char that is not VIN-legal and not an auto-correctable I/O/Q — specifically `*`, spaces, dashes, punctuation. 2. **Add a mask-aware error string**, e.g.: - `search.errorMaskedVin` (tr): *"VIN numarası maskelenmiş görünüyor (yıldız/eksik karakter içeriyor). Lütfen tam 17 karakterlik şase numarasını girin."* - `search.errorMaskedVin` (en): *"This VIN looks masked (it contains `*` or hidden characters). Please enter the full 17-character VIN."* Branch to it in `handleSearch` when the rejected input contains `*` or other non-alphanumerics, instead of always using `errorInvalidVin`. 3. **Live feedback before click:** show an inline red border + helper text as soon as the field contains masked/illegal characters; optionally keep the submit button disabled in that state so the user gets feedback before clicking. 4. **(Defensive, backend)** Align `VinValidationPipe` message so an asterisk-containing VIN returns a mask-specific 400 reason too, for API parity. ## Acceptance criteria - [ ] Pasting a 17-char masked VIN containing `*` shows a **mask-specific** message (not the I/O/Q message). - [ ] The masked/illegal-char condition produces **inline feedback before submit** (red border + helper text). - [ ] A genuinely valid 17-char VIN still decodes with no new friction (no false positives on legal chars). - [ ] An invalid VIN that is the *wrong length* still shows the length message; an invalid VIN with *I/O/Q* still auto-corrects as today. - [ ] New i18n keys exist in both `tr.json` and `en.json`. - [ ] (Optional) Backend `VinValidationPipe` returns a mask-specific reason for asterisk VINs. ## Verification checklist (confirm "fixed") - [ ] **Playwright repro (pre-fix):** script that pastes `VF7S1VJZ*********` and asserts the OLD misleading message appears (captured before merge as the failing baseline). - [ ] **Playwright (post-fix):** same script now asserts the new mask-aware message + red border, and that the submit either stays disabled or surfaces the correct message. - [ ] **Regression cases pass:** (a) valid VIN `NMTKH4BX60R107701` decodes; (b) `vin` with lowercase i/o/q auto-corrects; (c) 16-char input shows length error. - [ ] **Data signal:** after deploy, `search_input_validation_failed` events with `error_type=invalid_chars` & `input_length=17` (the masked-VIN signature) **trend down**; ideally add a new `error_type=masked_vin` to make this directly measurable. - [ ] **No new rage-clicks** on `/dashboard/search` for sessions that hit the masked path (check session recordings / `$rageclick`). - [ ] Insight `cmpe55g5` in the panel does **not** recur (pipeline will auto-flag as `regressed` if it does). ## Notes / scope - The panel commit `9a479f9` ("distinguish client-side VIN validation rejects from provider failures") is in the **observability/panel** repo, not this product — it changes how insights are *classified*, not product VIN handling. **No product commit addresses masked VINs.** - Out of scope: server-side VIN provider behavior; the separate "OEM part code typed into the VIN box" UX gap (tracked separately / low signal). --- *Filed from Süper Panel insight triage (2026-06-03). Covers insight(s): [cmpe55g570](https://sp.semih.ai/insights/i/cmpe55g57000mflajghytzgb6). Canonical: `cmpe55g57000mflajghytzgb6`.*
root added the insight-drivensase-pilotseverity-P3uxfrontendvin-decodevalidation labels 2026-06-03 20:08:54 +03:00
root closed this issue 2026-06-03 20:21:58 +03:00
root reopened this issue 2026-06-03 20:22:00 +03:00
Author
Owner

🤖 Fusion task opened: FN-435

Triage queue: https://fusion.semih.ai/tasks/FN-435

This comment was posted automatically by the fusion-plugin-gitea-issues bridge. A Fusion agent will update this issue when the task moves to in-progress, in-review, or done.

🤖 Fusion task opened: `FN-435` Triage queue: https://fusion.semih.ai/tasks/FN-435 _This comment was posted automatically by the `fusion-plugin-gitea-issues` bridge. A Fusion agent will update this issue when the task moves to in-progress, in-review, or done._
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: root/sase.tr#89