웹사이트나 앱용 피드백 폼

웹사이트나 앱용 피드백 폼4분 분량

페이지, 브라우저, 재현 단계를 담는 버그 리포트 폼 추가하기

재현 단계와 심각도는 사람에게 묻고, URL, 브라우저, 뷰포트, 최근 오류는 페이지가 알아서 추가하는 버그 리포트 폼입니다.

Preet SutharIkiform 창업자

리포터에게는 그 사람이 아는 것만 물으세요. 무엇이 잘못됐는지, 직전에 무엇을 했는지, 얼마나 막히는지입니다. 나머지는 제출 시점에 페이지가 채우게 하세요. 페이지 URL, 브라우저의 user agent, 뷰포트, 최근 JavaScript 오류 몇 개입니다. 폼 백엔드로 POST하고 각 리포트를 웹훅으로 이슈 트래커에 전달하세요.

버그 리포트가 다른 점

버그 리포트는 재현할 수 있어야만 쓸모가 있는데, 리포터는 필요한 세부 정보를 거의 모릅니다. 어떤 브라우저를 쓰는지 물으면 "Chrome"이나 "그냥 기본 거"라고 답합니다. URL을 물으면 페이지 이름이 돌아옵니다. 그러니 폼을 둘로 나누세요. 리포터만 답할 수 있는 질문과, 페이지가 이미 알고 있는 맥락입니다.

버그 리포트에서 사람이 입력하는 부분과 페이지가 추가하는 부분리포터가 입력제출 시 페이지가 추가요약한 줄, 8~140자재현 단계직전에 한 일심각도blocker, major또는 minor스크린샷 링크선택, 파일이 아닌 URLpage_urlorigin과 경로, 쿼리제외user_agent브라우저와 OS뷰포트크기와 픽셀 비율recent_errors최근 5개, 호스트 제거버그 리포트에서 사람이 입력하는 부분과 페이지가 추가하는 부분리포터가 입력제출 시 페이지가 추가요약한 줄, 8~140자재현 단계직전에 한 일심각도blocker, major 또는 minor스크린샷 링크선택, 파일이 아닌 URLpage_urlorigin과 경로, 쿼리 제외user_agent브라우저와 OS뷰포트크기와 픽셀 비율recent_errors최근 5개, 호스트 제거
  1. 리포터가 한 줄 요약을 씁니다.
  2. 문제가 생기기 직전에 무엇을 했는지 설명합니다.
  3. 얼마나 심하게 막히는지 고릅니다.
  4. 스크린샷이나 녹화 링크를 붙여 넣을 수 있습니다.
  5. 제출 시 페이지가 쿼리 문자열을 뺀 자기 주소를 추가합니다.
  6. 브라우저의 user agent를 추가합니다.
  7. 창 크기와 픽셀 비율을 추가합니다.
  8. 페이지가 본 최근 JavaScript 오류 몇 개를 호스트 이름을 지우고 추가합니다.
리포터에게 보이는 것은 윗줄뿐입니다. 아랫줄은 hidden 입력으로, 보내기를 누르는 순간 채워집니다.

코드에 영향을 주는 제약은 세 가지입니다.

  • referrer는 페이지가 아닙니다. 브라우저는 기본적으로 크로스 사이트 POST에 origin만 보내므로 meta.referrer로는 경로를 알 수 없습니다. page_url을 직접 보내세요.
  • 서버 POST는 브라우저를 가립니다. Ikiform은 요청을 보낸 쪽의 user agent를 기록합니다. 서버 액션에서 보내면 그건 내 서버입니다. 방문자의 user-agent 헤더를 필드로 전달하세요.
  • 스택 트레이스는 링크투성이입니다. 프레임마다 스크립트 URL이 있고, 링크가 3개 이상이면 스팸 점수에 2가 더해집니다. 오류를 보내기 전에 호스트를 지우세요. 그러지 않으면 스크린샷 링크를 붙여 넣은 리포트가 스팸으로 넘어갈 수 있습니다.

필드

미리보기

Ikiform의 검증기를 브라우저에서 실행합니다. 아무것도 전송되지 않습니다.

얼마나 막히나요?
마지막 다섯 필드는 실제 폼에서 hidden 입력입니다. 필드로 정의하면 Ikiform이 다른 필드처럼 길이와 타입을 검사합니다.

심각도는 우선순위 레이블이 아니라 리포터에게 미치는 영향으로 표현합니다. 사람들은 "더 진행할 수 없음"은 정확하게 고르지만 "Critical"은 무엇에든 고릅니다. 재현 단계는 최소 20자라서 "고장 났어요"가 대응할 수 있는 문장으로 바뀝니다.

맥락 수집하기

브라우저는 사람이 입력을 마친 뒤 submit 핸들러에서 hidden 필드를 채우므로, 리포트를 쓰는 동안 발생한 오류도 목록에 포함됩니다.

폼은 일반적인 방식으로 POST하고, 스크립트는 submit 중 브라우저가 요청을 만들기 전에 hidden 입력만 채웁니다. 빌드 시 <html> 요소에 data-app-version을 설정하세요. 브라우저가 직접 POST하므로 Ikiform의 meta.user_agent가 user_agent 필드와 일치합니다.

report-bug.html
<form id="bug" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="summary">What went wrong?</label>
  <input id="summary" name="summary" required minlength="8" maxlength="140" />
  <label for="steps">What did you do right before?</label>
  <textarea id="steps" name="steps" rows="5" required minlength="20"></textarea>
  <label for="expected">What did you expect to happen?</label>
  <textarea id="expected" name="expected" rows="3"></textarea>
  <fieldset>
    <legend>How much does it block you?</legend>
    <label><input type="radio" name="severity" value="blocker" required /> I can't continue</label>
    <label><input type="radio" name="severity" value="major" /> I found a workaround</label>
    <label><input type="radio" name="severity" value="minor" /> It's cosmetic</label>
  </fieldset>
  <label for="screenshot_url">Link to a screenshot or recording</label>
  <input id="screenshot_url" name="screenshot_url" type="url" />
  <label for="email">Email for follow-up questions</label>
  <input id="email" name="email" type="email" />
  <input type="hidden" name="page_url" />
  <input type="hidden" name="user_agent" />
  <input type="hidden" name="viewport" />
  <input type="hidden" name="app_version" />
  <input type="hidden" name="recent_errors" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send report</button>
</form>

<script>
  const recent = [];
  function remember(text) {
    recent.push(String(text).replace(/https?:\/\/[^/\s]+/g, "").slice(0, 400));
    if (recent.length > 5) recent.shift();
  }
  window.addEventListener("error", (event) => remember(`${event.message} at ${event.filename}:${event.lineno}`));
  window.addEventListener("unhandledrejection", (event) => remember(`Unhandled: ${event.reason?.stack ?? event.reason}`));

  document.getElementById("bug").addEventListener("submit", (event) => {
    const fields = event.currentTarget.elements;
    fields.page_url.value = location.origin + location.pathname;
    fields.user_agent.value = navigator.userAgent;
    fields.viewport.value = `${innerWidth}x${innerHeight} @${devicePixelRatio}x`;
    fields.app_version.value = document.documentElement.dataset.appVersion ?? "";
    fields.recent_errors.value = recent.join("\n---\n");
  });
</script>
서버 측 버전은 시크릿 키로 POST하므로 Ikiform이 Allowed domains와 사람 확인을 건너뜁니다. 그러면 모든 POST가 서버의 IP에서 오고, 방문자당 분당 5회인 Standard 한도가 모든 방문자를 합쳐서 계산합니다. 서버를 거쳐 POST하는 폼에는 Relaxed 또는 Custom 레이트 리밋을 고르고, IKIFORM_SECRET_KEY는 서버 전용 환경 변수에 두세요.

성공 메시지에 제출 ID가 표시됩니다. 리포터가 나중에 지원팀에 연락하면 그 ID로 한 번에 리포트를 찾을 수 있습니다. 버그 리포트 템플릿에는 맥락 수집 없이 더 짧은 버전이 있습니다.

이슈 트래커로 보내기

리포트는 수정이 추적될 곳에 있어야 합니다. 이슈를 만드는 릴레이를 가리키는 웹훅을 추가하세요. summary는 제목으로, 재현 단계와 맥락은 본문으로, severity는 레이블로 씁니다. Linear와 GitHub Issues 가이드에 서명 확인과 재시도를 갖춘 릴레이가 있습니다. 웹훅 문서에 페이로드가 담는 모든 meta 값이 나와 있습니다. 재현 단계가 잘 읽히도록 수집한 필드는 이슈 본문 끝의 접힌 블록에 넣으세요.

필드이슈에서
summary제목
steps, expected본문, 맨 앞
severity레이블. blocker를 필터링할 수 있습니다
page_url, user_agent, viewport, app_version재현 단계 아래의 details 블록
recent_errors코드 블록, 맨 끝

작동 확인하기

  1. 데스크톱 브라우저에서 페이지를 열고 콘솔에서 undefinedFunction()을 실행한 뒤 리포트를 보내세요.
  2. 대시보드에서 제출을 여세요. recent_errors에 https:// 호스트 없이 ReferenceError가 들어 있습니다.
  3. 서버 버전에서도 user_agent에 node가 아니라 내 브라우저가 표시됩니다.
  4. page_url에는 경로는 있지만 쿼리 문자열은 없습니다.
  5. 10자짜리 steps로 리포트를 보내세요. Ikiform이 steps를 지목하는 422로 응답하고, 폼이 메시지를 표시합니다.

주의할 점

  • 파일 업로드는 아직 지원하지 않습니다. 파일이 담긴 POST는 422 files_not_supported를 받습니다. 스크린샷 링크를 요청하거나, 이미지를 먼저 자체 스토리지에 올리고 그 URL을 제출하세요.
  • 각 값은 최대 16 KB입니다. 400자로 자른 오류 다섯 개는 그보다 훨씬 작지만, 콘솔 전체를 덤프하면 넘을 수 있습니다.
  • Chromium 브라우저는 macOS 버전처럼 user agent 문자열의 일부를 고정합니다. macOS 버전은 항상 10_15_7로 나옵니다. 분류할 때는 OS 버전이 아니라 브라우저 이름과 메이저 버전을 쓰세요.
  • 다른 origin의 스크립트에서 난 오류는 그 스크립트가 CORS 헤더와 함께 제공되고 crossorigin으로 로드되지 않는 한 Script error.만 표시됩니다.
  • 폼 내용, 로컬 스토리지, 쿠키를 리포트에 담지 마세요. 비밀번호와 토큰이 들어 있을 수 있고, 그것을 평문으로 저장하게 됩니다.

문제 해결

문제해결 방법
422 validation_failedPOST가 폼의 필드 정의와 맞지 않습니다. error.fields에 각 필드와 이유가 나옵니다. 입력의 name을 필드 ID와 맞추세요.
422 files_not_supported폼에 파일이 선택된 <input type="file">이 있습니다. 파일 업로드는 아직 지원하지 않습니다. 입력을 지우거나 대신 링크를 요청하세요.
스크린샷 링크가 있는 리포트가 스팸으로 분류됨모든 필드의 링크 수를 세어 보세요. 3개 이상이면 점수가 더해지고, 같은 리포트가 반복되면 임계값에 닿습니다. 오류에서 호스트를 지우고 리포트를 스팸 아님으로 표시하세요.
403 origin_not_allowed, 또는 모든 POST가 스팸으로 분류됨페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록된 도메인의 하위 도메인은 허용됩니다.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.