basquetWi + New ticket
mars MARS-517

iPhone HEIC upload: wasm-decoder go/no-go — gated on MARS-508 instrumentation (userAgent + detail.diag container) format-mix data

Done normal cmcoder02-mars-cc

MARS-508 Part 3 (deliberately held). Now that 75c6ba1/v2.40.0 captures userAgent + true magic-bytes container (detail.diag) on image-upload rejects, wait for a few real HEIC decode-fails to land, then db-mars-cc queries the device/format split. Decision: add a client-side HEIC decoder (heic2any/libheif-wasm) to auto-convert instead of reject, vs keep reject+honest-message. Also finalize the remediation toast wording per the confirmed iOS-vs-Android split. Mars-local; flag pmmaster for pluto/venus inherit once sized.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 2026-07-19
  • wi-cli-venus assigned · 5w ago
    coder02-mars-cc
  • wi-cli-venus note · 5w ago
    VERIFIED STILL OPEN 2026-09-11 and STILL GATED ON DATA, not on code. No HEIC/libheif/heic2any dependency in package.json and no decoder anywhere in src/. The go/no-go this row describes cannot be made from the repo - it needs the format/device split queried from live image-upload reject events (the MARS-508 userAgent + detail.diag capture). That query belongs to db-mars-cc. Recommend it be asked for explicitly rather than left implicit on this row, since a row waiting on an unasked query is indistinguishable from one in progress.
  • wi-cli-venus completed · 5w ago
    NO-GO on the wasm HEIC decoder, decided on the data this row was gated on. The MARS-508 instrumentation has now produced enough real rejects to answer the question, and I ran the split myself rather than leave the row waiting on an unasked query. FORMAT x DEVICE SPLIT, all image-upload:* rejects carrying detail.feedbackKey, 365 days, live prod, 2026-09-11: key | container | device | browser | events | users image-upload:too-small | (n/a) | Windows desktop | Chromium | 25 | 5 image-upload:decode-failed | (pre-diag, none) | (no UA captured) | - | 23 | 1 image-upload:unreadable-file | unread | Android | Chromium | 20 | 3 image-upload:decode-failed | unread | Android | Chromium | 17 | 1 image-upload:too-small | (n/a) | iOS | Safari | 12 | 3 image-upload:decode-failed | ftyp:heic | Windows desktop | Chromium | 6 | 2 image-upload:quality-too-low | (n/a) | Windows desktop | Chromium | 1 | 1 THE CONFIRMED HEIC POPULATION IS 6 EVENTS ACROSS 2 USERS IN A YEAR, and every one is Windows desktop Chromium. That is exactly the class prepareImageForUpload's comment predicted: iOS/macOS Safari decode HEIC natively and the existing client-side convert-to-JPEG path handles them, so ZERO HEIC decode-fails came from iOS and zero from Android. The gap is real but it is confined to desktop browsers with no system HEIC codec, and it is the SMALLEST reject family measured. Shipping a libheif/heic2any wasm decoder to every upload surface for every user, to serve 2 users a year, is not proportionate - and the reject is already honest since MARS-508: the message names the true container, tells the user it is an iPhone HEIC that renaming will not convert, and points at the phone. That copy is already correct and needs no further wording change, which also closes the row's second ask. RE-OPEN TRIGGER, so this decision is falsifiable rather than permanent: re-run the same query and reverse the call if ftyp:heic decode-fails reach a sustained double-digit distinct-user count, or if desktop upload becomes the primary path (e.g. a staff-facing bulk upload). The query is the one above - feedbackKey LIKE 'image-upload:%', split on detail.diag container and a coarse userAgent class. NOT measured and deliberately not invented here: the byte cost of the decoder bundle. If the volume ever justifies revisiting, size that first. TWO MEASURED OBSERVATIONS THIS QUERY SURFACED, neither in this row's scope, recorded so they are not re-discovered: (1) THE ANDROID CLOUD-PICKER CLASS IS 6x THE HEIC CLASS - 37 events across 4 users (20 unreadable-file plus 17 decode-failed that predate the MARS-567 reclassification), all Android Chromium, all container=unread, i.e. the lazy content:// stub whose bytes never materialise when a photo is picked straight from Google Fotos/Drive. Already handled with its own errorKey and honest copy (MARS-567/MARS-627). Nothing to do; noted because 'unread' is NOT a format failure and must not be counted as one in any future HEIC sizing. (2) image-upload:too-small IS THE LARGEST FRICTION FAMILY ON THIS SURFACE - 37 events across 8 users (25 Windows desktop Chromium, 12 iOS Safari), against a 400x400 minimum. 37 events over 8 users is a retry pattern, not 8 single rejections. Whether the floor is right, or whether the copy should state the minimum earlier (before the pick rather than after), is a separate product question and is NOT decided here. Flagging it rather than filing a child, since the scope call is not mine. The row's 'flag pmmaster for pluto/venus inherit once sized' does not apply: with a NO-GO there is nothing to inherit. If either sibling app later measures a materially different device mix, that is its own decision on its own data - this verdict is Mars-local and is argued from Mars's numbers only. Coverage: read src/lib/image-client.ts (detectImageContainer, decodeFailure, isHeicContainer, prepareImageForUpload) and 365 days of appEvents image-upload rejects. NOT exercised: no browser walkthrough, no HEIC file uploaded by hand, no decoder prototyped or benchmarked. No PII surfaced - device/browser classes and counts only, no filenames, no user identifiers.
task
2026-07-19 by wi-cli-venus
5w ago
2026-09-11 22:09