1// Prefilled GitHub-issue link for the on-site "Report a bug" button (#450). 2// 3// The archive is a static site, so a bug report is a deep link into GitHub's 4// new-issue form (.github/ISSUE_TEMPLATE/bug_report.yml). We prefill the form's 5// page, archive-version, and browser fields by their field ids so each report 6// carries the debug context the reporter would otherwise have to type (and 7// usually can't â most people don't know their user-agent string). 8// 9// The `page-context`, `archive-version`, and `browser` query keys below MUST 10// match the `id:` values in bug_report.yml; GitHub maps query parameters onto 11// issue-form fields by id. The build step is kept pure (context passed in) so it 12// unit-tests without a browser. 13 14const NEW_ISSUE_URL = 'https://github.com/jamditis/rosen-frontend/issues/new'; 15const TEMPLATE = 'bug_report.yml'; 16const LABELS = 'bug,user-report'; 17 18// The archive version is the ?v= cache-buster this module was imported with, so 19// it always matches the deployed build with no fetch and no separate constant 20// to keep in sync. Falls back to 'unknown' when loaded without the query (tests, 21// direct file access). 22export const ARCHIVE_VERSION = 23 new URL(import.meta.url).searchParams.get('v') || 'unknown'; 24 25/** 26 * Build the prefilled new-issue URL. 27 * @param {Object} ctx 28 * @param {string} [ctx.page] full page URL the reporter was on 29 * @param {string} [ctx.version] archive version string 30 * @param {string} [ctx.browser] navigator.userAgent 31 * @returns {string} an absolute github.com new-issue URL 32 */ 33export const buildBugReportUrl = ({ page = '', version = '', browser = '' } = {}) => { 34 const params = new URLSearchParams({ 35 template: TEMPLATE, 36 labels: LABELS, 37 'page-context': page, 38 'archive-version': version, 39 browser, 40 }); 41 return `${NEW_ISSUE_URL}?${params.toString()}`; 42}; 43 44/**
45 * Open a prefilled bug report in a new tab, reading the live page context at 46 * click time so the URL reflects the route and any open record (?record=). 47 */ 48export const openBugReport = () => { 49 const url = buildBugReportUrl({ 50 page: window.location.href, 51 version: ARCHIVE_VERSION, 52 browser: navigator.userAgent, 53 }); 54 window.open(url, '_blank', 'noopener,noreferrer'); 55}; 56 57const RECORD_LABELS = 'record-suggestion,user-report'; 58 59const trimField = (v) => (typeof v === 'string' ? v.trim() : ''); 60 61// Insert a zero-width space after each @ so a reader's typed text cannot ping a 62// GitHub user or team once they submit the prefilled issue. Mirrors the server's 63// neutralizeMentions_ (Code.gs), and built with fromCharCode so no invisible 64// byte sits in source. Applied only to free-form prose: structured fields (url, 65// email, page) stay raw so GitHub autolinks them and a handle URL like 66// medium.com/@user is not corrupted. 67const ZWSP = String.fromCharCode(0x200B); 68const bluntMentions = (v) => trimField(v).replace(/@/g, '@' + ZWSP); 69// First line only, so a multiline title cannot inject a heading, quote, or fake 70// field into the issue body once it renders on GitHub. 71const firstLine = (v) => trimField(v).split('\n')[0]; 72 73// GitHub rejects issue titles longer than 256 characters, so a prefilled link 74// carrying a longer title (a 300-char entered title, or a 2000-char url used as 75// the title) opens an issue the reader cannot save. Cap the title portion at the 76// server's limit (Code.gs truncateReport_, 120) so every fallback link is 77// savable. Mirrors that helper: slice to max-1 and append a single ellipsis. 78const REPORT_TITLE_MAX = 120; 79const truncateTitle = (v) => 80 v.length > REPORT_TITLE_MAX ? v.slice(0, REPORT_TITLE_MAX - 1) + 'â¦' : v; 81 82/** 83 * Build a prefilled new-issue URL for a report the branded modal could not send 84 * to the backend (pre-deploy state, or an endpoint outage). Intent-aware and 85 * data-preserving: a "suggest a record" report goes to a record issue carrying 86 * its url/title/why, not the bug template (which would drop that data), and a 87 * "report a problem" report carries the typed what-happened/expected/steps into 88 * the bug form so the reader never has to retype what they just entered. 89 * 90 * @param {Object} opts 91 * @param {'problem'|'record'} [opts.intent] 92 * @param {Object} [opts.fields] the entered form fields 93 * @param {Object} [opts.context] { page, version, browser } 94 * @returns {string} an absolute github.com new-issue URL 95 */ 96export const buildReportFallbackUrl = ({ intent = 'problem', fields = {}, context = {} } = {}) => { 97 const page = trimField(context.page); 98 const version = trimField(context.version); 99 const browser = trimField(context.browser); 100 101 if (intent === 'record') { 102 const url = trimField(fields.url); 103 const title = firstLine(fields.title); 104 const why = bluntMentions(fields.why); 105 const email = trimField(fields.email); 106 const body = ['Suggested by a reader via the in-archive form.', '', `Link: ${url}`]; 107 if (title) body.push(`Suggested title: ${bluntMentions(title)}`); 108 if (why) body.push('', 'Why it belongs:', why); 109 if (email) body.push('', `Contact: ${email}`); 110 body.push('', `Page: ${page}`, `Archive version: ${version}`); 111 const params = new URLSearchParams({ 112 title: `[record] ${truncateTitle(title || url)}`, 113 labels: RECORD_LABELS, 114 body: body.join('\n'), 115 }); 116 return `${NEW_ISSUE_URL}?${params.toString()}`; 117 } 118 119 const params = new URLSearchParams({ 120 template: TEMPLATE, 121 labels: LABELS, 122 'what-happened': bluntMentions(fields.whatHappened), 123 expected: bluntMentions(fields.expected), 124 steps: bluntMentions(fields.steps), 125 // Structured, so kept raw (GitHub does not mention an email's domain). 126 contact: trimField(fields.email), 127 'page-context': page, 128 'archive-version': version, 129 browser, 130 }); 131 return `${NEW_ISSUE_URL}?${params.toString()}`; 132}; 133 134/** 135 * Open the intent-aware fallback issue in a new tab. The modal calls this when 136 * the backend submit is unavailable, passing the reader's entered fields. 137 */ 138export const openReportFallback = ({ intent, fields, context } = {}) => { 139 const url = buildReportFallbackUrl({ intent, fields, context }); 140 window.open(url, '_blank', 'noopener,noreferrer'); 141};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.