Include hardware details in feedback submissions (#1726)

Add chip (e.g. Apple M1 Pro), RAM, hardware model, architecture
(arm64/x86_64), and display info to feedback metadata. All fields are
non-sensitive system properties collected via sysctlbyname, ProcessInfo,
and NSScreen. Server-side route accepts and renders the new fields in
both plain text and HTML email bodies.

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-18 02:59:16 -07:00 committed by GitHub
parent 0a99bb504c
commit de1aa7a6ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 3 deletions

View file

@ -8400,6 +8400,11 @@ private struct FeedbackComposerAppMetadata {
let bundleIdentifier: String
let osVersion: String
let localeIdentifier: String
let hardwareModel: String
let chip: String
let memoryGB: String
let architecture: String
let displayInfo: String
static var current: FeedbackComposerAppMetadata {
let infoDictionary = Bundle.main.infoDictionary ?? [:]
@ -8414,9 +8419,50 @@ private struct FeedbackComposerAppMetadata {
appCommit: commit ?? "",
bundleIdentifier: Bundle.main.bundleIdentifier ?? "",
osVersion: ProcessInfo.processInfo.operatingSystemVersionString,
localeIdentifier: Locale.preferredLanguages.first ?? Locale.current.identifier
localeIdentifier: Locale.preferredLanguages.first ?? Locale.current.identifier,
hardwareModel: sysctlString("hw.model") ?? "",
chip: sysctlString("machdep.cpu.brand_string") ?? "",
memoryGB: formatMemoryGB(),
architecture: currentArchitecture(),
displayInfo: currentDisplayInfo()
)
}
private static func sysctlString(_ name: String) -> String? {
var size = 0
guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { return nil }
var buffer = [CChar](repeating: 0, count: size)
guard sysctlbyname(name, &buffer, &size, nil, 0) == 0 else { return nil }
return String(cString: buffer).trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func formatMemoryGB() -> String {
let bytes = ProcessInfo.processInfo.physicalMemory
let gb = Double(bytes) / (1024 * 1024 * 1024)
return "\(Int(gb)) GB"
}
private static func currentArchitecture() -> String {
#if arch(arm64)
return "arm64"
#elseif arch(x86_64)
return "x86_64"
#else
return "unknown"
#endif
}
private static func currentDisplayInfo() -> String {
let screens = NSScreen.screens
let descriptions = screens.map { screen -> String in
let frame = screen.frame
let scale = screen.backingScaleFactor
return "\(Int(frame.width))x\(Int(frame.height)) @\(Int(scale))x"
}
let count = screens.count
let prefix = "\(count) display\(count == 1 ? "" : "s")"
return "\(prefix), \(descriptions.joined(separator: "; "))"
}
}
private enum FeedbackComposerSubmissionError: Error {
@ -8470,6 +8516,11 @@ private enum FeedbackComposerClient {
appendField("bundleIdentifier", value: metadata.bundleIdentifier, to: &body, boundary: boundary)
appendField("osVersion", value: metadata.osVersion, to: &body, boundary: boundary)
appendField("locale", value: metadata.localeIdentifier, to: &body, boundary: boundary)
appendField("hardwareModel", value: metadata.hardwareModel, to: &body, boundary: boundary)
appendField("chip", value: metadata.chip, to: &body, boundary: boundary)
appendField("memoryGB", value: metadata.memoryGB, to: &body, boundary: boundary)
appendField("architecture", value: metadata.architecture, to: &body, boundary: boundary)
appendField("displayInfo", value: metadata.displayInfo, to: &body, boundary: boundary)
for attachment in preparedAttachments {
appendFile(

View file

@ -32,6 +32,11 @@ const feedbackSchema = z.object({
bundleIdentifier: z.string().trim().max(200).optional().default(""),
osVersion: z.string().trim().max(200).optional().default(""),
locale: z.string().trim().max(120).optional().default(""),
hardwareModel: z.string().trim().max(120).optional().default(""),
chip: z.string().trim().max(200).optional().default(""),
memoryGB: z.string().trim().max(20).optional().default(""),
architecture: z.string().trim().max(20).optional().default(""),
displayInfo: z.string().trim().max(200).optional().default(""),
});
type PreparedAttachment = {
@ -83,6 +88,11 @@ export async function POST(request: Request) {
bundleIdentifier: getString(formData, "bundleIdentifier"),
osVersion: getString(formData, "osVersion"),
locale: getString(formData, "locale"),
hardwareModel: getString(formData, "hardwareModel"),
chip: getString(formData, "chip"),
memoryGB: getString(formData, "memoryGB"),
architecture: getString(formData, "architecture"),
displayInfo: getString(formData, "displayInfo"),
});
if (!parsed.success) {
@ -96,8 +106,10 @@ export async function POST(request: Request) {
return attachmentsResult.errorResponse;
}
const { appBuild, appCommit, appVersion, bundleIdentifier, email, locale, message, osVersion } =
parsed.data;
const {
appBuild, appCommit, appVersion, architecture, bundleIdentifier, chip,
displayInfo, email, hardwareModel, locale, memoryGB, message, osVersion,
} = parsed.data;
const subject = buildSubject(email, message, appVersion);
const attachments = attachmentsResult.attachments;
const resend = new Resend(feedbackConfig.resendApiKey);
@ -116,6 +128,11 @@ export async function POST(request: Request) {
bundleIdentifier,
osVersion,
locale,
hardwareModel,
chip,
memoryGB,
architecture,
displayInfo,
attachments,
}),
html: buildHtmlBody({
@ -127,6 +144,11 @@ export async function POST(request: Request) {
bundleIdentifier,
osVersion,
locale,
hardwareModel,
chip,
memoryGB,
architecture,
displayInfo,
attachments,
}),
attachments: attachments.map((attachment) => ({
@ -241,6 +263,11 @@ function buildTextBody(input: {
bundleIdentifier: string;
osVersion: string;
locale: string;
hardwareModel: string;
chip: string;
memoryGB: string;
architecture: string;
displayInfo: string;
attachments: PreparedAttachment[];
}) {
const attachmentLines =
@ -262,6 +289,11 @@ function buildTextBody(input: {
`Bundle identifier: ${input.bundleIdentifier || "unknown"}`,
`macOS: ${input.osVersion || "unknown"}`,
`Locale: ${input.locale || "unknown"}`,
`Hardware model: ${input.hardwareModel || "unknown"}`,
`Chip: ${input.chip || "unknown"}`,
`Memory: ${input.memoryGB || "unknown"}`,
`Architecture: ${input.architecture || "unknown"}`,
`Displays: ${input.displayInfo || "unknown"}`,
attachmentLines,
"",
"Message:",
@ -278,6 +310,11 @@ function buildHtmlBody(input: {
bundleIdentifier: string;
osVersion: string;
locale: string;
hardwareModel: string;
chip: string;
memoryGB: string;
architecture: string;
displayInfo: string;
attachments: PreparedAttachment[];
}) {
const attachmentMarkup =
@ -304,6 +341,11 @@ function buildHtmlBody(input: {
)}</p>
<p><strong>macOS:</strong> ${escapeHtml(input.osVersion || "unknown")}</p>
<p><strong>Locale:</strong> ${escapeHtml(input.locale || "unknown")}</p>
<p><strong>Hardware model:</strong> ${escapeHtml(input.hardwareModel || "unknown")}</p>
<p><strong>Chip:</strong> ${escapeHtml(input.chip || "unknown")}</p>
<p><strong>Memory:</strong> ${escapeHtml(input.memoryGB || "unknown")}</p>
<p><strong>Architecture:</strong> ${escapeHtml(input.architecture || "unknown")}</p>
<p><strong>Displays:</strong> ${escapeHtml(input.displayInfo || "unknown")}</p>
${attachmentMarkup}
<h2 style="font-size:15px;margin:24px 0 8px">Message</h2>
<pre style="white-space:pre-wrap;font:13px/1.6 SFMono-Regular,Menlo,monospace;background:#f3f4f6;border-radius:10px;padding:12px">${escapeHtml(