-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
714 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import Dependencies | ||
import Foundation | ||
import TelemetryClient | ||
import XCTestDynamicOverlay | ||
import OSLog | ||
|
||
struct AppTelemetryClient: Sendable { | ||
var initialize: @Sendable () -> Void | ||
var send: @Sendable (AppTelemetrySignal) -> Void | ||
|
||
func send(_ signalType: String) { | ||
send(AppTelemetrySignal(type: signalType)) | ||
} | ||
} | ||
|
||
extension AppTelemetryClient: TestDependencyKey { | ||
static let testValue = AppTelemetryClient( | ||
initialize: unimplemented("\(Self.self).initialize"), | ||
send: unimplemented("\(Self.self).send") | ||
) | ||
|
||
static let previewValue = AppTelemetryClient( | ||
initialize: { log.debug("initialize") }, | ||
send: { log.debug("send \($0.type)\($0.payload.isEmpty ? "" : " \($0.payload)")") } | ||
) | ||
} | ||
|
||
extension DependencyValues { | ||
var appTelemetry: AppTelemetryClient { | ||
get { self[AppTelemetryClient.self] } | ||
set { self[AppTelemetryClient.self] = newValue } | ||
} | ||
} | ||
|
||
extension AppTelemetryClient: DependencyKey { | ||
static let liveValue = AppTelemetryClient( | ||
initialize: { | ||
guard !TelemetryManager.isInitialized, let appID = Self.appID() else { return } | ||
TelemetryManager.initialize(with: .init(appID: appID)) | ||
}, | ||
send: { signal in | ||
guard TelemetryManager.isInitialized else { return } | ||
TelemetryManager.send( | ||
signal.type, | ||
for: signal.clientUser, | ||
floatValue: signal.floatValue, | ||
with: signal.payload | ||
) | ||
} | ||
) | ||
|
||
private static let log = Logger( | ||
subsystem: Bundle.main.bundleIdentifier!, | ||
category: "AppTelemetryClient" | ||
) | ||
|
||
private static func appID() -> String? { | ||
let data = Data(PackageResources.TelemetryDeckAppID) | ||
let string = String(data: data, encoding: .utf8)? | ||
.trimmingCharacters(in: .whitespacesAndNewlines) | ||
guard let string, !string.isEmpty else { | ||
log.fault("Missing TelemetryDeck AppID (app/Sources/AppFeature/Secrets/TelemetryDeckAppID)") | ||
return nil | ||
} | ||
return string | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import ContactFeature | ||
import Foundation | ||
import Mastodon | ||
import ProjectsFeature | ||
|
||
protocol AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { get } | ||
} | ||
|
||
extension Contact.Link: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { id.appTelemetryPayload } | ||
} | ||
|
||
extension Contact.Link.ID: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { ["contact.link.id": rawValue] } | ||
} | ||
|
||
extension Project: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String : String] { id.appTelemetryPayload } | ||
} | ||
|
||
extension Project.ID: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { | ||
["project.id": "\(DateFormatter.yearMonthDay.string(from: date)) \(name)"] | ||
} | ||
} | ||
|
||
extension Mastodon.Status: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { id.appTelemetryPayload } | ||
} | ||
|
||
extension Mastodon.Status.ID: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { ["mastodon.status.id": rawValue] } | ||
} | ||
|
||
extension Mastodon.MediaAttachment: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { id.appTelemetryPayload } | ||
} | ||
|
||
extension Mastodon.MediaAttachment.ID: AppTelemetryPayloadProviding { | ||
var appTelemetryPayload: [String: String] { ["mastodon.media-attachment.id": rawValue] } | ||
} | ||
|
||
private extension DateFormatter { | ||
static let yearMonthDay: DateFormatter = { | ||
let formatter = DateFormatter() | ||
formatter.dateFormat = "yyyy-MM-dd" | ||
formatter.timeZone = TimeZone(secondsFromGMT: 0) | ||
return formatter | ||
}() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import ComposableArchitecture | ||
import Foundation | ||
import TelemetryClient | ||
|
||
typealias AppTelemetryReducerOf<R: Reducer> = AppTelemetryReducer<R.State, R.Action> | ||
|
||
struct AppTelemetryReducer<State, Action>: Reducer { | ||
@Dependency(\.appTelemetry) var appTelemetry | ||
|
||
func reduce(into _: inout State, action: Action) -> Effect<Action> { | ||
let action = UncheckedSendable(action) | ||
return .run(priority: .low) { send in | ||
appTelemetry.send(.init( | ||
type: describe(action.wrappedValue), | ||
payload: payload(for: action.wrappedValue) | ||
)) | ||
} | ||
} | ||
|
||
private func describe(_ value: Any, abbreviated: Bool = false) -> String { | ||
let mirror = Mirror(reflecting: value) | ||
let prefix = (abbreviated && !(value is Error)) ? "" : typeName(type(of: value)) | ||
switch mirror.displayStyle { | ||
case .enum: | ||
if let child = mirror.children.first { | ||
let childLabel = child.label ?? "" | ||
let childOutput = describe(child.value, abbreviated: true) | ||
.nonEmpty.map { "(\($0))" } ?? "" | ||
return "\(prefix).\(childLabel)\(childOutput)" | ||
} else { | ||
return "\(prefix).\(value)" | ||
} | ||
case .optional: | ||
if let child = mirror.children.first { | ||
return ".some(\(describe(child.value, abbreviated: true)))" | ||
} else { | ||
return "\(prefix).none" | ||
} | ||
case .tuple: | ||
return mirror.children.map { label, value in | ||
let childLabel = toupleChildLabel(label).map { "\($0):" } | ||
let childOutput = describe(value, abbreviated: true).nonEmpty | ||
return [childLabel, childOutput].compactMap { $0 } .joined(separator: " ") | ||
} | ||
.joined(separator: ", ") | ||
default: | ||
return typeName(mirror.subjectType) | ||
} | ||
} | ||
|
||
private func typeName(_ type: Any.Type) -> String { | ||
var name = _typeName(type, qualified: true) | ||
if let index = name.firstIndex(of: ".") { | ||
name.removeSubrange(...index) | ||
} | ||
let sanitizedName = name.replacingOccurrences( | ||
of: #"<.+>|\(unknown context at \$[[:xdigit:]]+\)\."#, | ||
with: "", | ||
options: .regularExpression | ||
) | ||
return sanitizedName | ||
} | ||
|
||
private func payload(for value: Any) -> [String: String] { | ||
var payload: [String: String] = [:] | ||
if let error = value as? Error { | ||
payload["error.localizedDescription"] = error.localizedDescription | ||
let nsError = error as NSError | ||
payload["error.domain"] = nsError.domain | ||
payload["error.code"] = "\(nsError.code)" | ||
} | ||
if let providedPayload = (value as? AppTelemetryPayloadProviding)?.appTelemetryPayload { | ||
payload.addPayload(providedPayload) | ||
} | ||
let mirror = Mirror(reflecting: value) | ||
switch mirror.displayStyle { | ||
case .enum: | ||
if let child = mirror.children.first { | ||
let childPayload = self.payload(for: child.value) | ||
payload.addPayload(childPayload) | ||
} | ||
case .optional: | ||
if let child = mirror.children.first { | ||
payload = self.payload(for: child.value) | ||
} | ||
case .tuple: | ||
for (_, value) in mirror.children { | ||
let childPayload = self.payload(for: value) | ||
payload.addPayload(childPayload) | ||
} | ||
default: | ||
break | ||
} | ||
return payload | ||
} | ||
|
||
private func toupleChildLabel(_ label: String?) -> String? { | ||
guard let label else { return nil } | ||
let isUnlabeled = label.matches(of: #/^\.[0-9]+$/#).first != nil | ||
return isUnlabeled ? nil : label | ||
} | ||
} | ||
|
||
private extension Collection { | ||
var nonEmpty: Self? { isEmpty ? nil : self } | ||
} | ||
|
||
private extension [String: String] { | ||
mutating func addPayload(_ payload: [String: String]) { | ||
for (key, value) in payload { | ||
addPayload(key, value) | ||
} | ||
} | ||
|
||
mutating func addPayload(_ key: String, _ value: String) { | ||
if self[key] == nil { | ||
self[key] = value | ||
} else if let match = key.matches(of: #/^(?<key>.*)_(?<number>[0-9]+)$/#).first { | ||
let duplicateKey = match.output.key | ||
let nextNumber = (Int(match.output.number) ?? 0) + 1 | ||
addPayload("\(duplicateKey)_\(nextNumber)", value) | ||
} else { | ||
addPayload("\(key)_1", value) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
struct AppTelemetrySignal: Equatable, Sendable { | ||
var type: String | ||
var clientUser: String? | ||
var floatValue: Double? | ||
var payload: [String: String] = [:] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.