mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-15 00:34:08 +09:00
feat: added telemetry module
This commit is contained in:
93
Kit/plugins/Telemetry.swift
Normal file
93
Kit/plugins/Telemetry.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Telemetry.swift
|
||||
// Kit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 18/06/2023
|
||||
// Using Swift 5.0
|
||||
// Running on macOS 13.4
|
||||
//
|
||||
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private struct Report: Codable {
|
||||
let deviceID: UUID
|
||||
|
||||
let modules: [String]
|
||||
|
||||
let language: String?
|
||||
let device: String?
|
||||
let macOS: String?
|
||||
let version: String?
|
||||
}
|
||||
|
||||
public class Telemetry {
|
||||
public var isEnabled: Bool {
|
||||
get {
|
||||
self._isEnabled
|
||||
}
|
||||
set {
|
||||
self.toggle(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private var url: URL = URL(string: "https://api.serhiy.io/v1/stats/telemetry")!
|
||||
|
||||
private var _isEnabled: Bool = true
|
||||
|
||||
private let id: UUID
|
||||
private let repeater = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.Telemetry")
|
||||
private var modules: UnsafePointer<[Module]>
|
||||
|
||||
public init(_ modules: UnsafePointer<[Module]>) {
|
||||
self._isEnabled = Store.shared.bool(key: "telemetry", defaultValue: true)
|
||||
self.id = UUID(uuidString: Store.shared.string(key: "telemetry_id", defaultValue: UUID().uuidString)) ?? UUID()
|
||||
self.modules = modules
|
||||
|
||||
if !Store.shared.exist(key: "telemetry_id") {
|
||||
Store.shared.set(key: "telemetry_id", value: self.id.uuidString)
|
||||
self.toggle(self.isEnabled)
|
||||
}
|
||||
|
||||
self.report()
|
||||
}
|
||||
|
||||
@objc public func report() {
|
||||
guard self.isEnabled else { return }
|
||||
|
||||
let obj: Report = Report(
|
||||
deviceID: self.id,
|
||||
modules: self.modules.pointee.filter({ $0.available && $0.enabled }).compactMap({ $0.name }),
|
||||
language: Locale.current.languageCode,
|
||||
device: SystemKit.shared.device.model.id,
|
||||
macOS: SystemKit.shared.device.os?.version.getFullVersion(),
|
||||
version: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
)
|
||||
let jsonData = try? JSONEncoder().encode(obj)
|
||||
|
||||
var request = URLRequest(url: self.url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = jsonData
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request)
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private func toggle(_ newValue: Bool) {
|
||||
self._isEnabled = newValue
|
||||
Store.shared.set(key: "telemetry", value: newValue)
|
||||
|
||||
self.repeater.invalidate()
|
||||
|
||||
if newValue {
|
||||
self.repeater.repeats = true
|
||||
self.repeater.interval = 60 * 60 * 24
|
||||
self.repeater.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in
|
||||
self.report()
|
||||
completion(NSBackgroundActivityScheduler.Result.finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
5C23BC0A29A0EDA300DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0929A0EDA300DBA990 /* portal.swift */; };
|
||||
5C23BC0C29A10BE000DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0B29A10BE000DBA990 /* portal.swift */; };
|
||||
5C23BC1029A3B5AE00DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0F29A3B5AE00DBA990 /* portal.swift */; };
|
||||
5C5647F82A3F6B100098FFE9 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */; };
|
||||
5C8E001029269C7F0027C75A /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; };
|
||||
5CFE492A29264DF1000F2856 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE492929264DF1000F2856 /* main.swift */; };
|
||||
5CFE493929265055000F2856 /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; };
|
||||
@@ -390,6 +391,7 @@
|
||||
5C23BC0929A0EDA300DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
||||
5C23BC0B29A10BE000DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
||||
5C23BC0F29A3B5AE00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
||||
5C5647F72A3F6B100098FFE9 /* Telemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = "<group>"; };
|
||||
5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = eu.exelban.Stats.SMC.Helper; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5CFE492929264DF1000F2856 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
5CFE493829265055000F2856 /* protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = protocol.swift; sourceTree = "<group>"; };
|
||||
@@ -935,6 +937,7 @@
|
||||
9A6EEBBD2685259500897371 /* Logger.swift */,
|
||||
9A5A8446271895B700BC40A4 /* Reachability.swift */,
|
||||
9A302613286A2A3B00B41D57 /* Repeater.swift */,
|
||||
5C5647F72A3F6B100098FFE9 /* Telemetry.swift */,
|
||||
);
|
||||
path = plugins;
|
||||
sourceTree = "<group>";
|
||||
@@ -1726,6 +1729,7 @@
|
||||
9A2847612666AA2700EC1F6D /* PieChart.swift in Sources */,
|
||||
9A2847672666AA2700EC1F6D /* BarChart.swift in Sources */,
|
||||
9A28477B2666AA5000EC1F6D /* popup.swift in Sources */,
|
||||
5C5647F82A3F6B100098FFE9 /* Telemetry.swift in Sources */,
|
||||
9A2848202666AB3600EC1F6D /* types.swift in Sources */,
|
||||
9A28481E2666AB3600EC1F6D /* extensions.swift in Sources */,
|
||||
9A2848092666AB3000EC1F6D /* Store.swift in Sources */,
|
||||
|
||||
@@ -33,6 +33,7 @@ var modules: [Module] = [
|
||||
Bluetooth(),
|
||||
Clock()
|
||||
]
|
||||
let telemetry: Telemetry = Telemetry(&modules)
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
@@ -49,6 +49,7 @@ class ApplicationSettings: NSStackView {
|
||||
private var startAtLoginBtn: NSButton?
|
||||
private var uninstallHelperButton: NSButton?
|
||||
private var buttonsContainer: NSStackView?
|
||||
private var telemetryBtn: NSButton?
|
||||
|
||||
private var combinedModules: NSView?
|
||||
private var combinedModulesSeparator: NSView?
|
||||
@@ -88,6 +89,7 @@ class ApplicationSettings: NSStackView {
|
||||
|
||||
public func viewWillAppear() {
|
||||
self.startAtLoginBtn?.state = LaunchAtLogin.isEnabled ? .on : .off
|
||||
self.telemetryBtn?.state = telemetry.isEnabled ? .on : .off
|
||||
|
||||
var idx = self.updateSelector?.indexOfSelectedItem ?? 0
|
||||
if let items = self.updateSelector?.menu?.items {
|
||||
@@ -202,6 +204,14 @@ class ApplicationSettings: NSStackView {
|
||||
text: localizedString("Start at login")
|
||||
)
|
||||
grid.addRow(with: [NSGridCell.emptyContentView, self.startAtLoginBtn!])
|
||||
|
||||
self.telemetryBtn = self.toggleView(
|
||||
action: #selector(self.toggleTelemetry),
|
||||
state: telemetry.isEnabled,
|
||||
text: localizedString("Share anonymous telemetry")
|
||||
)
|
||||
grid.addRow(with: [NSGridCell.emptyContentView, self.telemetryBtn!])
|
||||
|
||||
grid.addRow(with: [NSGridCell.emptyContentView, self.toggleView(
|
||||
action: #selector(self.toggleCombinedModules),
|
||||
state: self.combinedModulesState,
|
||||
@@ -417,6 +427,10 @@ class ApplicationSettings: NSStackView {
|
||||
self.combinedModulesSpacing = key
|
||||
NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: nil)
|
||||
}
|
||||
|
||||
@objc private func toggleTelemetry(_ sender: NSButton) {
|
||||
telemetry.isEnabled = sender.state == NSControl.StateValue.on
|
||||
}
|
||||
}
|
||||
|
||||
private class ModuleSelectorView: NSStackView {
|
||||
@@ -550,7 +564,7 @@ internal class ModulePreview: NSStackView {
|
||||
|
||||
self.wantsLayer = true
|
||||
self.layer?.cornerRadius = 2
|
||||
self.layer?.borderColor = NSColor(hexString: "#dddddd").cgColor
|
||||
self.layer?.borderColor = NSColor(red: 221/255, green: 221/255, blue: 221/255, alpha: 1).cgColor
|
||||
self.layer?.borderWidth = 1
|
||||
self.layer?.backgroundColor = NSColor.white.cgColor
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ internal class SetupWindow: NSWindow, NSWindowDelegate {
|
||||
}
|
||||
|
||||
private class SetupContainer: NSStackView {
|
||||
private let pages: [NSView] = [SetupView_1(), SetupView_2(), SetupView_3(), SetupView_4()]
|
||||
private let pages: [NSView] = [SetupView_1(), SetupView_2(), SetupView_3(), SetupView_4(), SetupView_end()]
|
||||
|
||||
private var main: NSView = NSView()
|
||||
private var prevBtn: NSButton = NSButton()
|
||||
@@ -373,6 +373,70 @@ private class SetupView_3: NSStackView {
|
||||
}
|
||||
|
||||
private class SetupView_4: NSStackView {
|
||||
init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60))
|
||||
|
||||
let container: NSGridView = NSGridView()
|
||||
container.rowSpacing = 0
|
||||
container.yPlacement = .center
|
||||
container.xPlacement = .center
|
||||
|
||||
let title: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 22))
|
||||
title.alignment = .center
|
||||
title.font = NSFont.systemFont(ofSize: 20, weight: .semibold)
|
||||
title.stringValue = localizedString("Share anonymous telemetry for better development decisions")
|
||||
title.toolTip = localizedString("Share anonymous telemetry for better development decisions")
|
||||
title.isSelectable = false
|
||||
|
||||
container.addRow(with: [title])
|
||||
container.addRow(with: [self.content()])
|
||||
|
||||
container.row(at: 0).height = 100
|
||||
|
||||
self.addArrangedSubview(container)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func content() -> NSView {
|
||||
let container: NSGridView = NSGridView()
|
||||
|
||||
container.addRow(with: [self.option(
|
||||
tag: 1,
|
||||
state: telemetry.isEnabled,
|
||||
text: localizedString("Share anonymous telemetry data")
|
||||
)])
|
||||
container.addRow(with: [self.option(
|
||||
tag: 2,
|
||||
state: !telemetry.isEnabled,
|
||||
text: localizedString("Do not share anonymous telemetry data")
|
||||
)])
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
private func option(tag: Int, state: Bool, text: String) -> NSView {
|
||||
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 30, height: 20))
|
||||
button.setButtonType(.radio)
|
||||
button.state = state ? .on : .off
|
||||
button.title = text
|
||||
button.action = #selector(self.toggle)
|
||||
button.isBordered = false
|
||||
button.isTransparent = false
|
||||
button.target = self
|
||||
button.tag = tag
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func toggle(_ sender: NSButton) {
|
||||
telemetry.isEnabled = sender.tag == 1
|
||||
}
|
||||
}
|
||||
|
||||
private class SetupView_end: NSStackView {
|
||||
init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user