Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions macOS/DuckDuckGo-macOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -4438,6 +4438,18 @@
FEE12B412E520E4D00AD9807 /* PromoHistoryRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE12B412E520E4D00AD9808 /* PromoHistoryRecordTests.swift */; };
FEE12B412E520E4D00AD9809 /* PromoHistoryRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE12B412E520E4D00AD9808 /* PromoHistoryRecordTests.swift */; };
FFF6627FC03B410786EF2635 /* RebrandedContextualOnboardingDialogs+SearchCompleted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E776DE170ED47F4B3E992DF /* RebrandedContextualOnboardingDialogs+SearchCompleted.swift */; };
E5A6E743849E63F8101AC616 /* QuickFeedbackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A043BAE1603803A839C0CB52 /* QuickFeedbackService.swift */; };
F8FD0BA84A167FE482ED4A92 /* QuickFeedbackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A043BAE1603803A839C0CB52 /* QuickFeedbackService.swift */; };
C8C8050EED40EF171CC2784D /* QuickFeedbackDiagnosticsCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F511D9CB8F68D9D9B0341314 /* QuickFeedbackDiagnosticsCollector.swift */; };
638B34FA781359331A3ED13A /* QuickFeedbackDiagnosticsCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F511D9CB8F68D9D9B0341314 /* QuickFeedbackDiagnosticsCollector.swift */; };
D16140583C0346C5BE2D9B96 /* QuickFeedbackWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6323C46A8F7693E56A2AD5F8 /* QuickFeedbackWindowController.swift */; };
1222238CCD1D2149036D6651 /* QuickFeedbackWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6323C46A8F7693E56A2AD5F8 /* QuickFeedbackWindowController.swift */; };
EEB674AA4DB3CA34C17F48ED /* QuickFeedbackTipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76403F87E9F53E2C9FFA007 /* QuickFeedbackTipController.swift */; };
C8E4E6A94A9FA76E5ABF3A2A /* QuickFeedbackTipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76403F87E9F53E2C9FFA007 /* QuickFeedbackTipController.swift */; };
6F2449E6DE7EE721370575B9 /* QuickFeedbackDiagnosticsCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD3D561804AB2C73BB789AC /* QuickFeedbackDiagnosticsCollectorTests.swift */; };
8A96290053C27B6C1245838B /* QuickFeedbackDiagnosticsCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD3D561804AB2C73BB789AC /* QuickFeedbackDiagnosticsCollectorTests.swift */; };
AD1ABD21CA2DB9C9505A3217 /* QuickFeedbackTipControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30149F658F94CFC627227DB /* QuickFeedbackTipControllerTests.swift */; };
FB06A1C37CD1DEDC550642FE /* QuickFeedbackTipControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30149F658F94CFC627227DB /* QuickFeedbackTipControllerTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -6820,6 +6832,12 @@
FD23FD2A28816606007F6985 /* AutoconsentMessageProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AutoconsentMessageProtocolTests.swift; path = UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift; sourceTree = SOURCE_ROOT; };
FD23FD2C2886A81D007F6985 /* AutoconsentManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoconsentManagement.swift; sourceTree = "<group>"; };
FEE12B412E520E4D00AD9808 /* PromoHistoryRecordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoHistoryRecordTests.swift; sourceTree = "<group>"; };
A043BAE1603803A839C0CB52 /* QuickFeedbackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickFeedbackService.swift; sourceTree = "<group>"; };
F511D9CB8F68D9D9B0341314 /* QuickFeedbackDiagnosticsCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickFeedbackDiagnosticsCollector.swift; sourceTree = "<group>"; };
6323C46A8F7693E56A2AD5F8 /* QuickFeedbackWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickFeedbackWindowController.swift; sourceTree = "<group>"; };
C76403F87E9F53E2C9FFA007 /* QuickFeedbackTipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickFeedbackTipController.swift; sourceTree = "<group>"; };
ABD3D561804AB2C73BB789AC /* QuickFeedbackDiagnosticsCollectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickFeedbackDiagnosticsCollectorTests.swift; sourceTree = "<group>"; };
C30149F658F94CFC627227DB /* QuickFeedbackTipControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickFeedbackTipControllerTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -10403,6 +10421,7 @@
AA3863C227A1E1C000749AB5 /* Feedback */ = {
isa = PBXGroup;
children = (
A5534EA92B26FA8ADC30543F /* QuickFeedback */,
BB53CF3C2E26CED5007F0B42 /* New */,
372042E02DF9C04400CE099A /* Resources */,
AA3D531827A2F24C00074EC1 /* View */,
Expand Down Expand Up @@ -12149,6 +12168,7 @@
BB0036602E426E9D0016DDEF /* Feedback */ = {
isa = PBXGroup;
children = (
C416159555D0D98B0D468E38 /* QuickFeedback */,
BB00365C2E426E9D0016DDEF /* Model */,
BB00365F2E426E9D0016DDEF /* New */,
);
Expand Down Expand Up @@ -12926,6 +12946,26 @@
path = Subscription;
sourceTree = "<group>";
};
A5534EA92B26FA8ADC30543F /* QuickFeedback */ = {
isa = PBXGroup;
children = (
A043BAE1603803A839C0CB52 /* QuickFeedbackService.swift */,
F511D9CB8F68D9D9B0341314 /* QuickFeedbackDiagnosticsCollector.swift */,
6323C46A8F7693E56A2AD5F8 /* QuickFeedbackWindowController.swift */,
C76403F87E9F53E2C9FFA007 /* QuickFeedbackTipController.swift */,
);
path = QuickFeedback;
sourceTree = "<group>";
};
C416159555D0D98B0D468E38 /* QuickFeedback */ = {
isa = PBXGroup;
children = (
ABD3D561804AB2C73BB789AC /* QuickFeedbackDiagnosticsCollectorTests.swift */,
C30149F658F94CFC627227DB /* QuickFeedbackTipControllerTests.swift */,
);
path = QuickFeedback;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -15857,6 +15897,10 @@
4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */,
842FE5472F16691D002F8ABC /* WarnBeforeQuitOverlayPresenter.swift in Sources */,
DFE9DC288642264669587656 /* WebViewUserAgentProvider.swift in Sources */,
E5A6E743849E63F8101AC616 /* QuickFeedbackService.swift in Sources */,
C8C8050EED40EF171CC2784D /* QuickFeedbackDiagnosticsCollector.swift in Sources */,
D16140583C0346C5BE2D9B96 /* QuickFeedbackWindowController.swift in Sources */,
EEB674AA4DB3CA34C17F48ED /* QuickFeedbackTipController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -16324,6 +16368,8 @@
5681ED442BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */,
BB0036652E426EDD0016DDEF /* ReportProblemFormViewModelTests.swift in Sources */,
BB0036662E426EDD0016DDEF /* FeedbackTests.swift in Sources */,
6F2449E6DE7EE721370575B9 /* QuickFeedbackDiagnosticsCollectorTests.swift in Sources */,
AD1ABD21CA2DB9C9505A3217 /* QuickFeedbackTipControllerTests.swift in Sources */,
BB0036672E426EDD0016DDEF /* RequestNewFeatureViewModelTests.swift in Sources */,
C1F142DB2CB93AED003DA518 /* FreemiumDBPPromotionViewCoordinatorTests.swift in Sources */,
1C4C0269DA0A67A1081B7FCA /* FreemiumDBPPromoDelegateTests.swift in Sources */,
Expand Down Expand Up @@ -18085,6 +18131,10 @@
B68D21C32ACBC916002DA3C2 /* ContentBlockingMock.swift in Sources */,
AA5C1DD1285A154E0089850C /* RecentlyClosedMenu.swift in Sources */,
D1EC9FDAACABFE5B574FD557 /* WebViewUserAgentProvider.swift in Sources */,
F8FD0BA84A167FE482ED4A92 /* QuickFeedbackService.swift in Sources */,
638B34FA781359331A3ED13A /* QuickFeedbackDiagnosticsCollector.swift in Sources */,
1222238CCD1D2149036D6651 /* QuickFeedbackWindowController.swift in Sources */,
C8E4E6A94A9FA76E5ABF3A2A /* QuickFeedbackTipController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -18182,6 +18232,8 @@
BB0036622E426E9D0016DDEF /* RequestNewFeatureViewModelTests.swift in Sources */,
CC6E897D2E5F08EA00F6BED0 /* SessionRestorePromptCoordinatorTests.swift in Sources */,
BB0036642E426E9D0016DDEF /* FeedbackTests.swift in Sources */,
8A96290053C27B6C1245838B /* QuickFeedbackDiagnosticsCollectorTests.swift in Sources */,
FB06A1C37CD1DEDC550642FE /* QuickFeedbackTipControllerTests.swift in Sources */,
8434D95A2F23747700B4796F /* TerminationDeciderHandlerTests.swift in Sources */,
B69B50462726C5C200758A2B /* AtbAndVariantCleanupTests.swift in Sources */,
567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */,
Expand Down
11 changes: 11 additions & 0 deletions macOS/DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private(set) var promoService: PromoService?
var privacyDashboardWindow: NSWindow?

@MainActor private(set) lazy var quickFeedbackService: QuickFeedbackService = {
let diagnosticsCollector = QuickFeedbackDiagnosticsCollector(
tabCountProvider: windowControllersManager,
launchDate: appLaunchDate
)
return QuickFeedbackService(
diagnosticsCollector: diagnosticsCollector,
firePublisher: fireCoordinator.fireViewModel.fire.burningDataPublisher
)
}()

let tabCrashAggregator = TabCrashAggregator()
let windowControllersManager: WindowControllersManager
let tabSuspensionService: TabSuspensionService
Expand Down
10 changes: 10 additions & 0 deletions macOS/DuckDuckGo/Application/AppVersionModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// limitations under the License.
//

import AppKit
import Common
import PrivacyConfig

Expand Down Expand Up @@ -71,4 +72,13 @@ final class AppVersionModel {
}
return label
}

/// Distribution channel label (e.g. "App Store", "DMG", "DMG Alpha").
var distributionLabel: String {
var label = NSApp.isSandboxed ? "App Store" : "DMG"
if buildType.isAlphaBuild {
label.append(" Alpha")
}
return label
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
// QuickFeedbackDiagnosticsCollector.swift
//
// Copyright © 2026 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import AppKit
import Foundation
import IOKit

final class QuickFeedbackDiagnosticsCollector {

private weak var tabCountProvider: TabCountProviding?
private let launchDate: Date

init(tabCountProvider: TabCountProviding? = nil, launchDate: Date = Date()) {
self.tabCountProvider = tabCountProvider
self.launchDate = launchDate
}

func collectDiagnostics() -> String {
var lines = [String]()

lines.append("--- Diagnostics (auto-collected) ---")

let appVersionModel = AppVersionModel()
lines.append("App Version: \(appVersionModel.versionLabelShort) (\(appVersionModel.distributionLabel))")

let osVersion = ProcessInfo.processInfo.operatingSystemVersion
lines.append("macOS: \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)")

#if arch(arm64)
lines.append("Architecture: Apple Silicon (arm64)")
#elseif arch(x86_64)
lines.append("Architecture: Intel (x86_64)")
#else
lines.append("Architecture: unknown")
#endif

lines.append("GPU: \(gpuSummary())")
lines.append("Memory: \(memorySummary())")
lines.append("Disk: \(diskSummary())")

if let provider = tabCountProvider {
lines.append("Tabs: \(provider.tabCount) tabs / \(provider.windowCount) windows")
}

lines.append("Session: \(sessionLength())")

return lines.joined(separator: "\n")
}

// MARK: - Private

private func gpuSummary() -> String {
var iterator: io_iterator_t = 0
let port: mach_port_t
if #available(macOS 12.0, *) {
port = kIOMainPortDefault
} else {
port = kIOMasterPortDefault
}
let result = IOServiceGetMatchingServices(port, IOServiceMatching("IOPCIDevice"), &iterator)
guard result == KERN_SUCCESS else { return "unknown" }
defer { IOObjectRelease(iterator) }

var names = [String]()
var entry: io_object_t = IOIteratorNext(iterator)
while entry != 0 {
if let modelData = IORegistryEntryCreateCFProperty(entry, "model" as CFString, kCFAllocatorDefault, 0)?.takeRetainedValue() as? Data,
let model = String(data: modelData.prefix(while: { $0 != 0 }), encoding: .utf8) {
names.append(model)
}
IOObjectRelease(entry)
entry = IOIteratorNext(iterator)
}

return names.isEmpty ? "unknown" : names.joined(separator: ", ")
}
Comment thread
cursor[bot] marked this conversation as resolved.

private func memorySummary() -> String {
let physicalGB = Double(ProcessInfo.processInfo.physicalMemory) / 1_073_741_824.0

var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<natural_t>.size)
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}

if result == KERN_SUCCESS {
let browserMB = info.resident_size / (1024 * 1024)
return String(format: "%llu MB browser, %.0f GB total", browserMB, physicalGB)
}
return String(format: "%.0f GB total", physicalGB)
}

private func diskSummary() -> String {
guard let homeURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first,
let values = try? homeURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]),
let freeBytes = values.volumeAvailableCapacityForImportantUsage else {
return "unknown"
}
let freeGB = freeBytes / (1024 * 1024 * 1024)
return "\(freeGB) GB free"
}

private func sessionLength() -> String {
let uptime = Date().timeIntervalSince(launchDate)
if uptime < 60 { return "under a minute" }
if uptime < 3600 { return "\(Int(uptime / 60)) minutes" }
if uptime < 86400 { return "\(Int(uptime / 3600)) hours" }
return "\(Int(uptime / 86400)) days"
}
}

protocol TabCountProviding: AnyObject {
var tabCount: Int { get }
var windowCount: Int { get }
}
Loading
Loading