Skip to content

Commit

Permalink
Make it easier to check for appLanguage, preferredLanguage, and regio…
Browse files Browse the repository at this point in the history
…n + grand rename (#153)

* Make it easier to check for appLanguage, preferredLanguage, and region

* Adjust new parameters names, introduce more & send alongside old names

* Fix tools version & provide proper support for visionOS

* Fix minor issue

* Apply grand rename by deprecating existing APIs and introducing new

* Bump version to new major version 2.0.0

* Shorten configuration parameter to config

* Add a renamed TelemetryDeck library while also keeping the old one

* Add TelemetryDeck target to fix potential import issues

* Make TelemetryDeck.signal method public

* Fix incorrect app language code detection on modern OSes

* Update Xcode version to 15 on CI

* Fix tests + specify package as scheme as it contains the test target

* Fix all SwiftLint warnings

* Adjust tests after fixing SwiftLint warnings + switch to Ubuntu-based SwiftLint on CI

* Update device destinations on CI builds + add Vision Pro
  • Loading branch information
Jeehut authored May 23, 2024
1 parent ab7455c commit 0c5dcdd
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 128 deletions.
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

0 comments on commit 0c5dcdd

Please sign in to comment.