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:
parent
0a99bb504c
commit
de1aa7a6ae
2 changed files with 96 additions and 3 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue