Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it easier to check for appLanguage, preferredLanguage, and region + grand rename #153

Merged
merged 16 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 10 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,28 @@ on:
jobs:
lint:
name: Lint Code
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: Repository checkout
uses: actions/checkout@v4
- name: Lint
run: swiftlint
uses: norio-nomura/[email protected]
with:
args: --strict
test:
name: Test Xcode ${{ matrix.xcode }} - ${{ matrix.xcodebuildCommand }}
runs-on: "macos-latest"
strategy:
fail-fast: true
matrix:
xcode:
- ^14
# - ^15
- ^15
xcodebuildCommand:
- "-sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14'"
- "-sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15'"
- "-sdk macosx -destination 'platform=macOS'"
- "-sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV 4K (2nd generation)'"
- "-sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)'"
- "-sdk xrsimulator -destination 'platform=visionOS Simulator,name=Apple Vision Pro'"
- "-sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)'"
- "-sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)'"
steps:
- name: Repository checkout
uses: actions/checkout@v4
Expand All @@ -38,4 +40,4 @@ jobs:
with:
xcode-version: ${{ matrix.xcode }}
- name: Build and Test
run: xcodebuild test -scheme TelemetryClient ${{ matrix.xcodebuildCommand }}
run: xcodebuild test -scheme TelemetryClient-Package ${{ matrix.xcodebuildCommand }}
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# By default, SwiftLint uses a set of sensible default rules you can adjust:
disabled_rules: # rule identifiers turned on by default to exclude from running
- identifier_name
- function_body_length
- opening_brace
- trailing_comma
Expand Down
26 changes: 11 additions & 15 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

// swift-tools-version:5.9
import PackageDescription

let package = Package(
Expand All @@ -9,22 +7,20 @@ let package = Package(
.macOS(.v10_13),
.iOS(.v12),
.watchOS(.v5),
.tvOS(.v13)
.tvOS(.v13),
.visionOS(.v1),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "TelemetryClient",
targets: ["TelemetryClient"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.library(name: "TelemetryDeck", targets: ["TelemetryDeck"]), // new name
.library(name: "TelemetryClient", targets: ["TelemetryClient"]), // old name
],
dependencies: [],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "TelemetryDeck",
dependencies: ["TelemetryClient"],
resources: [.copy("PrivacyInfo.xcprivacy")]
),
.target(
name: "TelemetryClient",
resources: [.copy("PrivacyInfo.xcprivacy")]
Expand Down
4 changes: 2 additions & 2 deletions Sources/TelemetryClient/CryptoHashing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ enum CryptoHashing {
/// should be preferred where available.
/// [CommonCrypto](https://github.com/apple-oss-distributions/CommonCrypto) provides compatibility with older OS versions,
/// apps built with Xcode versions lower than 11 and non-Apple platforms like Linux.
static func sha256(str: String, salt: String) -> String {
if let strData = (str + salt).data(using: String.Encoding.utf8) {
static func sha256(string: String, salt: String) -> String {
if let strData = (string + salt).data(using: String.Encoding.utf8) {
#if canImport(CryptoKit)
if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) {
let digest = SHA256.hash(data: strData)
Expand Down
182 changes: 152 additions & 30 deletions Sources/TelemetryClient/Signal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,35 +40,71 @@ internal struct SignalPostBody: Codable, Equatable {

/// The default payload that is included in payloads processed by TelemetryDeck.
public struct DefaultSignalPayload: Encodable {
public let platform = Self.platform
public let systemVersion = Self.systemVersion
public let majorSystemVersion = Self.majorSystemVersion
public let majorMinorSystemVersion = Self.majorMinorSystemVersion
public let appVersion = Self.appVersion
public let buildNumber = Self.buildNumber
public let isSimulator = "\(Self.isSimulator)"
public let isDebug = "\(Self.isDebug)"
public let isTestFlight = "\(Self.isTestFlight)"
public let isAppStore = "\(Self.isAppStore)"
public let modelName = Self.modelName
public let architecture = Self.architecture
public let operatingSystem = Self.operatingSystem
public let targetEnvironment = Self.targetEnvironment
public let locale = Self.locale
public let extensionIdentifier: String? = Self.extensionIdentifier
public let telemetryClientVersion = TelemetryClientVersion

public init() { }

public func toDictionary() -> [String: String] {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(self)
let dict = try JSONSerialization.jsonObject(with: data) as? [String: String]
return dict ?? [:]
} catch {
return [:]
public static var parameters: [String: String] {
var parameters: [String: String] = [
// deprecated names
"platform": Self.platform,
"systemVersion": Self.systemVersion,
"majorSystemVersion": Self.majorSystemVersion,
"majorMinorSystemVersion": Self.majorMinorSystemVersion,
"appVersion": Self.appVersion,
"buildNumber": Self.buildNumber,
"isSimulator": "\(Self.isSimulator)",
"isDebug": "\(Self.isDebug)",
"isTestFlight": "\(Self.isTestFlight)",
"isAppStore": "\(Self.isAppStore)",
"modelName": Self.modelName,
"architecture": Self.architecture,
"operatingSystem": Self.operatingSystem,
"targetEnvironment": Self.targetEnvironment,
"locale": Self.locale,
"region": Self.region,
"appLanguage": Self.appLanguage,
"preferredLanguage": Self.preferredLanguage,
"telemetryClientVersion": telemetryClientVersion,

// new names
"TelemetryDeck.AppInfo.buildNumber": Self.buildNumber,
"TelemetryDeck.AppInfo.version": Self.appVersion,
"TelemetryDeck.AppInfo.versionAndBuildNumber": "\(Self.appVersion) (build \(Self.buildNumber))",

"TelemetryDeck.Device.architecture": Self.architecture,
"TelemetryDeck.Device.modelName": Self.modelName,
"TelemetryDeck.Device.operatingSystem": Self.operatingSystem,
"TelemetryDeck.Device.orientation": Self.orientation,
"TelemetryDeck.Device.platform": Self.platform,
"TelemetryDeck.Device.screenResolutionHeight": Self.screenResolutionHeight,
"TelemetryDeck.Device.screenResolutionWidth": Self.screenResolutionWidth,
"TelemetryDeck.Device.systemMajorMinorVersion": Self.majorMinorSystemVersion,
"TelemetryDeck.Device.systemMajorVersion": Self.majorSystemVersion,
"TelemetryDeck.Device.systemVersion": Self.systemVersion,
"TelemetryDeck.Device.timeZone": Self.timeZone,

"TelemetryDeck.RunContext.isAppStore": "\(Self.isAppStore)",
"TelemetryDeck.RunContext.isDebug": "\(Self.isDebug)",
"TelemetryDeck.RunContext.isSimulator": "\(Self.isSimulator)",
"TelemetryDeck.RunContext.isTestFlight": "\(Self.isTestFlight)",
"TelemetryDeck.RunContext.language": Self.appLanguage,
"TelemetryDeck.RunContext.locale": Self.locale,
"TelemetryDeck.RunContext.targetEnvironment": Self.targetEnvironment,

"TelemetryDeck.SDK.name": "SwiftSDK",
"TelemetryDeck.SDK.nameAndVersion": "SwiftSDK \(telemetryClientVersion)",
"TelemetryDeck.SDK.version": telemetryClientVersion,

"TelemetryDeck.UserPreference.language": Self.preferredLanguage,
"TelemetryDeck.UserPreference.region": Self.region,
]

if let extensionIdentifier = Self.extensionIdentifier {
// deprecated name
parameters["extensionIdentifier"] = extensionIdentifier

// new name
parameters["TelemetryDeck.RunContext.extensionIdentifier"] = extensionIdentifier
}

return parameters
}
}

Expand Down Expand Up @@ -174,7 +210,7 @@ extension DefaultSignalPayload {
var modelIdentifier: String?

if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data {
if let modelIdentifierCString = String(data: modelData, encoding: .utf8)?.cString(using: .utf8) {
if let modelIdentifierCString = String(decoding: modelData, as: UTF8.self).cString(using: .utf8) {
modelIdentifier = String(cString: modelIdentifierCString)
}
}
Expand Down Expand Up @@ -271,8 +307,94 @@ extension DefaultSignalPayload {
#endif
}

/// The locale identifier
/// The locale identifier the app currently runs in. E.g. `en_DE` for an app that does not support German on a device with preferences `[German, English]`, and region Germany.
static var locale: String {
return Locale.current.identifier
}

/// The region identifier both the user most prefers and also the app is set to. They are always the same because formatters in apps always auto-adjust to the users preferences.
static var region: String {
if #available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *) {
return Locale.current.region?.identifier ?? Locale.current.identifier.components(separatedBy: .init(charactersIn: "-_")).last!
} else {
return Locale.current.regionCode ?? Locale.current.identifier.components(separatedBy: .init(charactersIn: "-_")).last!
}
}

/// The language identifier the app is currently running in. This represents the language the system (or the user) has chosen for the app to run in.
static var appLanguage: String {
if #available(iOS 16, macOS 13, tvOS 16, visionOS 1, watchOS 9, *) {
return Locale.current.language.languageCode?.identifier ?? Locale.current.identifier.components(separatedBy: .init(charactersIn: "-_"))[0]
} else {
return Locale.current.languageCode ?? Locale.current.identifier.components(separatedBy: .init(charactersIn: "-_"))[0]
}
}

/// The language identifier of the users most preferred language set on the device. Returns also languages the current app is not even localized to.
static var preferredLanguage: String {
let preferredLocaleIdentifier = Locale.preferredLanguages.first ?? "zz-ZZ"
return preferredLocaleIdentifier.components(separatedBy: .init(charactersIn: "-_"))[0]
}

/// The current devices screen resolution width in points.
static var screenResolutionWidth: String {
#if os(iOS) || os(tvOS)
return "\(UIScreen.main.bounds.width)"
#elseif os(watchOS)
return "\(WKInterfaceDevice.current().screenBounds.width)"
#elseif os(macOS)
if let screen = NSScreen.main {
return "\(screen.frame.width)"
}
return "Unknown"
#else
return "N/A"
#endif
}

/// The current devices screen resolution height in points.
static var screenResolutionHeight: String {
#if os(iOS) || os(tvOS)
return "\(UIScreen.main.bounds.height)"
#elseif os(watchOS)
return "\(WKInterfaceDevice.current().screenBounds.height)"
#elseif os(macOS)
if let screen = NSScreen.main {
return "\(screen.frame.height)"
}
return "Unknown"
#else
return "N/A"
#endif
}

/// The current devices screen orientation. Returns `Fixed` for devices that don't support an orientation change.
static var orientation: String {
#if os(iOS)
switch UIDevice.current.orientation {
case .portrait, .portraitUpsideDown:
return "Portrait"
case .landscapeLeft, .landscapeRight:
return "Landscape"
default:
return "Unknown"
}
#else
return "Fixed"
#endif
}

/// The devices current time zone in the modern `UTC` format, such as `UTC+1`, or `UTC-3:30`.
static var timeZone: String {
let secondsFromGMT = TimeZone.current.secondsFromGMT()
let hours = secondsFromGMT / 3600
let minutes = abs(secondsFromGMT / 60 % 60)

let sign = secondsFromGMT >= 0 ? "+" : "-"
if minutes > 0 {
return "UTC\(sign)\(hours):\(String(format: "%02d", minutes))"
} else {
return "UTC\(sign)\(hours)"
}
}
}
8 changes: 4 additions & 4 deletions Sources/TelemetryClient/SignalEnricher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import Foundation

public protocol SignalEnricher {
func enrich(
signalType: TelemetrySignalType,
signalType: String,
for clientUser: String?,
floatValue: Double?
) -> [String: String]
}

extension Dictionary where Key == String, Value == String {
func applying(_ rhs: [String: String]) -> [String: String] {
merging(rhs) { _, rhs in
rhs
func applying(_ other: [String: String]) -> [String: String] {
merging(other) { _, other in
other
}
}

Expand Down
Loading