PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/utils/bugReport.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:27:21 UTC 6,179 bytes, 141 lines download raw bytes

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.