{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"the-first-user-complaint","title":"The first user complaint","url":"https://addaly.com/learn/building-apps-with-ai/the-first-user-complaint","position":81,"of":87,"minutes":8,"answer":"A complaint becomes solvable when it can be joined to a log line, so every error shown to a user carries a reference id, every log line carries the same id.","keyIdea":"A complaint becomes solvable when it can be joined to a log line, so every error shown to a user carries a reference id, every log line carries the same id, and the fix ends with a new item in the regression set."},"body":{"format":"markdown","text":"## \"It doesn't work\"\n\nThat is the whole message, most of the time. No page, no time, no screenshot. It is not the user being unhelpful; it is what a problem looks like from the other side, where there is no log and no stack trace, only a button that did nothing.\n\nThe work of this lesson is turning that sentence into a line in a log, and then into a fix, and then into something that cannot happen again.\n\n## Before the first complaint: the reference id\n\nGenerate an id per request (a short random string) and put it in three places: the log line for anything that happens during that request, the error tracker's event, and the error message shown to the user:\n\n> Something went wrong saving your booking. Reference: `k7q2-4mzd`. It has been reported.\n\nNow a user who writes \"it said reference k7q2-4mzd\" has handed you the exact log line, and the search takes seconds. Most frameworks and hosts either provide a request id or make it a middleware of three lines. It is the highest-value ten minutes in this block.\n\n## The three questions\n\nWhen the message has no reference, ask three things, and only three, because each extra question halves the chance of an answer:\n\n1. Which page, or what were you trying to do?\n2. Roughly when: today, an hour ago?\n3. What did the screen show: an error, nothing, the wrong thing?\n\nWhile waiting, search. The email's timestamp and the user's account narrow the logs to a few minutes and one user id. A 500 in that window is the answer. A 200 in that window with no error means the third kind of broken from the previous lesson: it worked and it was wrong, and you need the user's description of what \"wrong\" was.\n\n## Seeing what they saw\n\nTwo tools for the case where the logs are clean.\n\n**View as user**, for admins: a way to open the app with a given user's data, read-only, with an audit row recording that you did. It shows the empty state they see, the booking they cannot find, the date that rendered wrongly for their time zone. Build it with the admin protections from the security block, because it is the most powerful page in the app.\n\n**A copy of their situation on the preview.** Their account's shape (a user with forty bookings, one cancelled, in a different time zone) recreated by the seed script against the preview deployment. This is the reproduction the breaking block insisted on, done with a real shape instead of a guessed one.\n\n## The reply\n\nOnce you know:\n\n> Found it: bookings made after 11pm were being saved with the next day's date because of a time zone mistake on our side. It is fixed as of this afternoon; your booking for the 30th is now showing correctly. Thank you for telling us.\n\nWhat happened, that it was yours, that it is fixed, and that their specific case is right now. No \"we apologise for any inconvenience\"; the sentence about their booking is the apology. If it is not fixed yet, say when you expect it to be and write again when it is.\n\n## The fix ends with an item\n\nThe breaking block ended a fix with the question \"was it actually fixed\". This block ends it one step later: the failing case becomes a permanent test. A row in the eval set if it was the AI feature, a test in the test suite if it was code, an entry in the three-account script if it was access. Every complaint that reaches you is an example nobody on your side thought of, and it is worth more as a test than as a memory.\n\n## The complaint that is not a bug\n\nSome are misunderstandings: the feature works and the user expected something else. That is still information (the label was unclear, the button was in the wrong place, the empty state did not say what to do) and it goes into the same list with a different tag. A feature that three people misunderstand the same way has a design bug, whatever the code says.\n\n## Keeping the list\n\nOne table or one document: date, user id, what they said, what it was, what changed. In three months it is the truest account of what the app does badly that exists, and the first thing a developer you bring in will ask to read."},"check":{"question":"A user emails: 'it doesn't work'. You have request ids in your logs and a report button, but the user used neither. What is the most productive first reply?","options":[{"text":"Ask three specific things (which page, roughly what time, and what they saw on screen) and meanwhile search the logs for their account around the time the email was sent.","correct":true},{"text":"Explain that you need them to use the report button on the page so that the technical details are captured automatically for you, and that without it there is little you can do to investigate.","correct":false,"tempting":"Correct about the button and wrong as a first reply; a user who has already written once will often not write twice, and the questions below get most of what the button would."},{"text":"Reply that you have not been able to reproduce it and ask them to try again later.","correct":false,"tempting":"Nothing has been tried; the reply spends the user's goodwill to buy nothing."},{"text":"Ask the agent to review the whole codebase for anything that could produce a failure.","correct":false,"tempting":"An unbounded search with no symptom; the user and the logs can narrow it to one route in minutes."}]},"prev":{"slug":"knowing-it-broke-before-users-tell-you","title":"Knowing it broke before users tell you","url":"https://addaly.com/learn/building-apps-with-ai/knowing-it-broke-before-users-tell-you"},"next":{"slug":"the-legal-minimum-of-a-public-site","title":"The legal minimum of a public site","url":"https://addaly.com/learn/building-apps-with-ai/the-legal-minimum-of-a-public-site"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The first user complaint', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/the-first-user-complaint (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The first user complaint","courseTitle":"Building Apps with AI","module":{"index":9,"slug":"what-it-costs-and-where-this-stops","title":"What it costs, running it, and where this stops"},"answer":null,"keyIdea":"A complaint becomes solvable when it can be joined to a log line, so every error shown to a user carries a reference id, every log line carries the same id, and the fix ends with a new item in the regression set.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"it-doesn-t-work","runs":[{"text":"“It doesn’t work”"}]},{"t":"p","runs":[{"text":"That is the whole message, most of the time. No page, no time, no screenshot. It is not the user being unhelpful; it is what a problem looks like from the other side, where there is no log and no stack trace, only a button that did nothing."}]},{"t":"p","runs":[{"text":"The work of this lesson is turning that sentence into a line in a log, and then into a fix, and then into something that cannot happen again."}]},{"t":"h2","id":"before-the-first-complaint-the-reference-id","runs":[{"text":"Before the first complaint: the reference id"}]},{"t":"p","runs":[{"text":"Generate an id per request (a short random string) and put it in three places: the log line for anything that happens during that request, the error tracker’s event, and the error message shown to the user:"}]},{"t":"quote","runs":[{"text":"Something went wrong saving your booking. Reference: "},{"text":"k7q2-4mzd","code":true},{"text":". It has been reported."}]},{"t":"p","runs":[{"text":"Now a user who writes “it said reference k7q2-4mzd” has handed you the exact log line, and the search takes seconds. Most frameworks and hosts either provide a request id or make it a middleware of three lines. It is the highest-value ten minutes in this block."}]},{"t":"h2","id":"the-three-questions","runs":[{"text":"The three questions"}]},{"t":"p","runs":[{"text":"When the message has no reference, ask three things, and only three, because each extra question halves the chance of an answer:"}]},{"t":"ol","start":1,"items":[[{"text":"Which page, or what were you trying to do?"}],[{"text":"Roughly when: today, an hour ago?"}],[{"text":"What did the screen show: an error, nothing, the wrong thing?"}]]},{"t":"p","runs":[{"text":"While waiting, search. The email’s timestamp and the user’s account narrow the logs to a few minutes and one user id. A 500 in that window is the answer. A 200 in that window with no error means the third kind of broken from the previous lesson: it worked and it was wrong, and you need the user’s description of what “wrong” was."}]},{"t":"h2","id":"seeing-what-they-saw","runs":[{"text":"Seeing what they saw"}]},{"t":"p","runs":[{"text":"Two tools for the case where the logs are clean."}]},{"t":"p","runs":[{"text":"View as user","b":true},{"text":", for admins: a way to open the app with a given user’s data, read-only, with an audit row recording that you did. It shows the empty state they see, the booking they cannot find, the date that rendered wrongly for their time zone. Build it with the admin protections from the security block, because it is the most powerful page in the app."}]},{"t":"p","runs":[{"text":"A copy of their situation on the preview.","b":true},{"text":" Their account’s shape (a user with forty bookings, one cancelled, in a different time zone) recreated by the seed script against the preview deployment. This is the reproduction the breaking block insisted on, done with a real shape instead of a guessed one."}]},{"t":"h2","id":"the-reply","runs":[{"text":"The reply"}]},{"t":"p","runs":[{"text":"Once you know:"}]},{"t":"quote","runs":[{"text":"Found it: bookings made after 11pm were being saved with the next day’s date because of a time zone mistake on our side. It is fixed as of this afternoon; your booking for the 30th is now showing correctly. Thank you for telling us."}]},{"t":"p","runs":[{"text":"What happened, that it was yours, that it is fixed, and that their specific case is right now. No “we apologise for any inconvenience”; the sentence about their booking is the apology. If it is not fixed yet, say when you expect it to be and write again when it is."}]},{"t":"h2","id":"the-fix-ends-with-an-item","runs":[{"text":"The fix ends with an item"}]},{"t":"p","runs":[{"text":"The breaking block ended a fix with the question “was it actually fixed”. This block ends it one step later: the failing case becomes a permanent test. A row in the eval set if it was the AI feature, a test in the test suite if it was code, an entry in the three-account script if it was access. Every complaint that reaches you is an example nobody on your side thought of, and it is worth more as a test than as a memory."}]},{"t":"h2","id":"the-complaint-that-is-not-a-bug","runs":[{"text":"The complaint that is not a bug"}]},{"t":"p","runs":[{"text":"Some are misunderstandings: the feature works and the user expected something else. That is still information (the label was unclear, the button was in the wrong place, the empty state did not say what to do) and it goes into the same list with a different tag. A feature that three people misunderstand the same way has a design bug, whatever the code says."}]},{"t":"h2","id":"keeping-the-list","runs":[{"text":"Keeping the list"}]},{"t":"p","runs":[{"text":"One table or one document: date, user id, what they said, what it was, what changed. In three months it is the truest account of what the app does badly that exists, and the first thing a developer you bring in will ask to read."}]}],"check":{"question":[{"text":"A user emails: ‘it doesn’t work’. You have request ids in your logs and a report button, but the user used neither. What is the most productive first reply?"}],"options":[{"text":[{"text":"Ask three specific things (which page, roughly what time, and what they saw on screen) and meanwhile search the logs for their account around the time the email was sent."}],"correct":true,"tempting":null},{"text":[{"text":"Explain that you need them to use the report button on the page so that the technical details are captured automatically for you, and that without it there is little you can do to investigate."}],"correct":false,"tempting":[{"text":"Correct about the button and wrong as a first reply; a user who has already written once will often not write twice, and the questions below get most of what the button would."}]},{"text":[{"text":"Reply that you have not been able to reproduce it and ask them to try again later."}],"correct":false,"tempting":[{"text":"Nothing has been tried; the reply spends the user’s goodwill to buy nothing."}]},{"text":[{"text":"Ask the agent to review the whole codebase for anything that could produce a failure."}],"correct":false,"tempting":[{"text":"An unbounded search with no symptom; the user and the logs can narrow it to one route in minutes."}]}],"order":[3,0,2,1],"why":null},"prev":{"slug":"knowing-it-broke-before-users-tell-you","title":"Knowing it broke before users tell you","minutes":8},"next":{"slug":"the-legal-minimum-of-a-public-site","title":"The legal minimum of a public site","minutes":9}}}