From de1aa7a6ae9d4f91871bf6dabb19ed49941df781 Mon Sep 17 00:00:00 2001
From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Date: Wed, 18 Mar 2026 02:59:16 -0700
Subject: [PATCH] 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
---
Sources/ContentView.swift | 53 ++++++++++++++++++++++++++++++++++-
web/app/api/feedback/route.ts | 46 ++++++++++++++++++++++++++++--
2 files changed, 96 insertions(+), 3 deletions(-)
diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift
index 181d1d0e..508b495f 100644
--- a/Sources/ContentView.swift
+++ b/Sources/ContentView.swift
@@ -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(
diff --git a/web/app/api/feedback/route.ts b/web/app/api/feedback/route.ts
index 33256634..96560d1c 100644
--- a/web/app/api/feedback/route.ts
+++ b/web/app/api/feedback/route.ts
@@ -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: {
)}
macOS: ${escapeHtml(input.osVersion || "unknown")}
Locale: ${escapeHtml(input.locale || "unknown")}
+ Hardware model: ${escapeHtml(input.hardwareModel || "unknown")}
+ Chip: ${escapeHtml(input.chip || "unknown")}
+ Memory: ${escapeHtml(input.memoryGB || "unknown")}
+ Architecture: ${escapeHtml(input.architecture || "unknown")}
+ Displays: ${escapeHtml(input.displayInfo || "unknown")}
${attachmentMarkup}
Message
${escapeHtml(