{"record":{"id":"7919d31698302884","repo":"koala73/worldmonitor","slug":"invalid-country","errorCode":"INVALID_COUNTRY","errorMessage":"INVALID_COUNTRY","messagePattern":"INVALID_COUNTRY","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/followedCountries.ts","lineNumber":347,"sourceCode":" *\n * Without Tier 1, two parallel first-ever mutations could both read\n * `meta=undefined`, both INSERT, and produce duplicate meta rows that\n * break the `.unique()` read AND re-open the cap-bypass window. With\n * Tier 1 in place, the brand-new-user race is closed deterministically.\n *\n * Errors are typed `ConvexError({kind, ...})` with object data so callers\n * can branch on `err.data.kind` (memory:\n * `convex-error-string-data-strips-errordata-on-wire`).\n */\nexport const followCountry = mutation({\n  args: { country: v.string() },\n  handler: async (ctx, args): Promise<FollowMutationResult> => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) throw new ConvexError({ kind: \"UNAUTHENTICATED\" });\n    const userId = identity.subject;\n\n    if (!isValidIso2(args.country)) {\n      throw new ConvexError({\n        kind: \"INVALID_COUNTRY\",\n        country: args.country,\n      });\n    }\n\n    // Tier-1 lock: pre-seeded shard row. Read at top, patch at end.\n    const shard = await readShardOrThrow(ctx, userId);\n\n    // Tier-2 read: per-user denormalized count (lazy-created, but safe\n    // under the shard lock above).\n    const { meta, count: currentCount } = await readUserMeta(ctx, userId);\n\n    const existingRow = await ctx.db\n      .query(\"followedCountries\")\n      .withIndex(\"by_user_country\", (q) =>\n        q.eq(\"userId\", userId).eq(\"country\", args.country),\n      )\n      .first();","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/followedCountries.ts#L329-L365","documentation":"Thrown by `followCountry` after the auth gate passes but `isValidIso2(args.country)` returns false. The mutation takes a single `country` string and requires it to be a valid ISO 3166-1 alpha-2 code (e.g. \"US\", \"JP\", \"DE\"). Carries object data `{ kind: \"INVALID_COUNTRY\", country: args.country }` so the client can echo back the offending value. This throw is intentionally NOT a return-value (unlike the FREE_CAP branch) — invalid input is a client bug and is wanted in Sentry.","triggerScenarios":"Calling `followCountry` with a non-ISO2 string: a full country name (\"United States\"), an ISO3 code (\"USA\"), lowercase (\"us\"), an empty string, a numeric code, or a typo (\"UX\"). Also a stale panel config referencing a retired/renamed code.","commonSituations":"Frontend passes the country display label instead of the code; an upstream data source emits ISO3 and the conversion was skipped; a user-follow action built from a free-text field; a code list that drifted from the ISO registry (e.g. \"XK\" for Kosovo, which is not in ISO 3166-1).","solutions":["Pass only validated ISO 3166-1 alpha-2 codes (uppercase, 2 letters) from a static registry, not from user input or display names.","Client-side: gate the follow button on `isValidIso2(country)` using the same registry the server uses.","On `err.data.kind === \"INVALID_COUNTRY\"`, log `err.data.country` to find which source emits bad codes.","Normalize upstream ISO3 -> ISO2 before calling (e.g. \"USA\" -> \"US\")."],"exampleFix":"// before\nawait convex.mutation(api.followedCountries.followCountry, { country: countryName });\n\n// after — send the validated ISO2 code\nconst ISO2 = new Set([\"US\",\"JP\",\"DE\",\"GB\",\"FR\",/* ... full registry */]);\nconst code = toIso2(countryName); // your normalizer\nif (!ISO2.has(code)) { console.warn(\"bad country\", countryName); return; }\nawait convex.mutation(api.followedCountries.followCountry, { country: code });","handlingStrategy":"validation","validationCode":"const ISO2 = new Set([\"US\",\"JP\",\"DE\",\"GB\",\"FR\",\"CA\",\"AU\",/* ... full ISO 3166-1 alpha-2 registry */]);\nfunction toValidIso2(code: string): string | null {\n  const c = code.trim().toUpperCase();\n  return ISO2.has(c) ? c : null;\n}\nconst code = toValidIso2(input);\nif (!code) { console.warn(\"invalid country\", input); return; }","typeGuard":"function isValidIso2(code: string): boolean {\n  return /^[A-Z]{2}$/.test(code) && ISO2_REGISTRY.has(code);\n}","tryCatchPattern":"try {\n  await convex.mutation(api.followedCountries.followCountry, { country });\n} catch (err) {\n  if (err.data?.kind === \"INVALID_COUNTRY\") console.warn(\"bad code\", err.data.country);\n  else throw err;\n}","preventionTips":["Maintain a single ISO2 registry shared by client and server.","Never pass display names or ISO3 codes to country mutations.","Normalize upstream ISO3/numeric/name to ISO2 before calling.","Log err.data.country to find the emitting source."],"tags":["validation","convex","country","iso2"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}