A User-Agent parser is production policy disguised as string handling. Its output can change a dashboard, a feature flag, a support route, or a security rule. That makes a confident wrong answer more dangerous than an explicit unknown. A parser that labels a WebView as Chrome may send a checkout investigation to the wrong team; a parser that labels a malformed request as a crawler may hide a real user failure.

Define the decision before the pattern

Start with the action the label controls. If the action is a browser compatibility note, family and major version may be enough. If the action is a mobile layout hint, mobile status and platform are more relevant. If the action blocks traffic, the parser needs a higher bar and probably another signal besides the UA.

Write the output contract in a small type rather than returning a pile of strings:

{
  family: 'chromium',
  major: 124,
  platform: 'android',
  container: 'webview',
  confidence: 'limited',
  reasons: ['webview marker', 'android platform token']
}

The reasons field makes a classification inspectable. It also creates a place to say why the parser stopped at unknown instead of forcing a best guess.

Precedence is where edge cases live

Build fixtures for iPad desktop mode, Android WebView, in-app browsers, crawler tokens, older Internet Explorer patterns, reduced Chrome, Firefox on iOS, and malformed headers. Then test precedence, not just whether each regex matches. A generic Safari marker should not erase a more specific embedded-browser marker. A crawler token should not win merely because it appears early in the string if the product is analysing a user report with another reliable signal.

Keep the fixture input and expected output next to the parser. A compact table in the review can show why a rule exists:

  • specific WebView marker before generic Chromium
  • in-app marker before platform-only fallback
  • reduced Chromium major accepted without invented minor version
  • malformed or conflicting values return limited confidence

Measure disagreements during a migration

When a parser changes, run old and new versions over a redacted sample set and store only the differences needed for review. Count family changes, platform changes, unknown changes, and version changes. A large drop in unknown may look like progress while actually showing that the new parser is overconfident.

Inspect the disagreement examples by consumer. A change that is harmless for a content chart can be harmful for a checkout fallback. Do not approve the parser solely because its unit tests pass; approve the decisions that depend on it.

Keep raw samples on a short leash

Raw UA strings can include app names, build clues, and request context that become sensitive when combined with other logs. Redact query values and account data before putting a sample in a fixture. Use a retention rule for raw observations, and keep normalized test cases when a long-term regression test is enough.

The parser should not become a fingerprinting tool. It can support a compatibility decision without collecting every token a browser exposes or claiming to know the person behind the request.

Unknown is an output, not an exception

Support needs to know when a label is incomplete. Analytics needs a stable bucket for unavailable detail. QA needs a route to a real reproduction. Return unknown or limited confidence when the evidence is insufficient, and make downstream code handle it intentionally.

Test the consumer contract

Parser tests should assert more than a family name. For a feature flag, verify the flag decision. For a support route, verify the message and owner. For a report, verify that unavailable detail stays in its own bucket. An illustrative fixture can have two input strings that produce the same Chromium major but different container values; the correct test asks whether checkout is routed to WebView investigation, not whether both strings contain Chrome.

Keep malformed, empty, conflicting, and deliberately long inputs in the suite. Check that the parser returns within a bounded time and does not echo the entire raw value into an error message. A parser sits close to request handling, so a harmless-looking diagnostic path can become a log-volume or privacy problem when a client sends unexpected data.

Release changes with an explanation

When shipping a new parser, publish a compact disagreement report: fixture count, family changes, platform changes, version changes, unknown changes, and examples that alter a product action. Record the old and new parser versions and the date. If a change is intentional, name the reason. If it is not understood, hold the rollout or keep the new result in a shadow field.

This discipline makes the word unknown operationally useful. It is not a failure to classify; it is a signal that the next decision needs another witness, such as a capability check, a controlled reproduction, or a user-provided context. The parser should make that next step visible.

Our standard is a parser that can explain itself: which rule matched, what it decided, what it could not decide, and how that output should be used. A short list of honest labels is more valuable than a long list of precise-looking guesses.