10bc457c72
Unify PiP recovery across startup and foreground transitions, surface actionable status, and add privacy-safe analytics plus the refreshed onboarding and account experience. Update documentation assets and advance the release build to 84.
139 lines
4.7 KiB
Swift
139 lines
4.7 KiB
Swift
// AnalyticsExtensionPrivacyTests.swift
|
|
// OSGKeyboardExtTests
|
|
//
|
|
// The keyboard analytics runtime is anonymous and emits only typed dimensions.
|
|
|
|
import Foundation
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class AnalyticsExtensionPrivacyTests: XCTestCase {
|
|
func testKeyboardRuntimeUploadsWithoutAuthorizationOrSensitiveInputFields() async throws {
|
|
let databaseURL = try temporaryDatabaseURL()
|
|
let network = ExtensionAnalyticsNetwork()
|
|
let runtime = AnalyticsRuntime.keyboardExtension(
|
|
environment: AnalyticsEnvironment(appVersion: "2.0.0", osVersion: "26.0"),
|
|
repositoryConfiguration: AnalyticsRepositoryConfiguration(
|
|
databaseURL: databaseURL
|
|
),
|
|
uploadConfiguration: AnalyticsUploadConfiguration(
|
|
endpoint: URL(string: "https://analytics.test/v1/events")!
|
|
),
|
|
network: network,
|
|
wallClock: ExtensionAnalyticsClock(),
|
|
monotonicClock: ExtensionAnalyticsMonotonicClock(),
|
|
uuidGenerator: ExtensionAnalyticsUUIDGenerator(),
|
|
random: ExtensionAnalyticsRandomGenerator()
|
|
)
|
|
|
|
runtime.client.recordKeyboardActivated()
|
|
for _ in 0..<200 {
|
|
if await runtime.repository.debugSnapshot().pendingEvents.count == 1 {
|
|
break
|
|
}
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
let pendingSnapshot = await runtime.repository.debugSnapshot()
|
|
XCTAssertEqual(pendingSnapshot.pendingEvents.count, 1)
|
|
|
|
await runtime.uploadCoordinator.uploadAvailableEvents()
|
|
|
|
let requests = await network.requests()
|
|
let request = try XCTUnwrap(requests.first)
|
|
XCTAssertEqual(requests.count, 1)
|
|
XCTAssertNil(
|
|
request.headers.first {
|
|
$0.key.caseInsensitiveCompare("Authorization") == .orderedSame
|
|
}
|
|
)
|
|
let upload = try JSONDecoder().decode(
|
|
AnalyticsUploadRequest.self,
|
|
from: request.body
|
|
)
|
|
XCTAssertEqual(upload.events.count, 1)
|
|
XCTAssertEqual(upload.events.first?.surface, .keyboard)
|
|
XCTAssertEqual(upload.events.first?.eventType, .keyboardActivated)
|
|
|
|
let root = try XCTUnwrap(
|
|
JSONSerialization.jsonObject(with: request.body) as? [String: Any]
|
|
)
|
|
let eventObjects = try XCTUnwrap(root["events"] as? [[String: Any]])
|
|
let event = try XCTUnwrap(eventObjects.first)
|
|
XCTAssertFalse(event.keys.contains("properties"))
|
|
XCTAssertFalse(event.keys.contains("text"))
|
|
XCTAssertFalse(event.keys.contains("prompt"))
|
|
XCTAssertFalse(event.keys.contains("transcript"))
|
|
}
|
|
|
|
private func temporaryDatabaseURL() throws -> URL {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("OSGKeyboardExtensionAnalyticsTests", isDirectory: true)
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(
|
|
at: directory,
|
|
withIntermediateDirectories: true
|
|
)
|
|
return directory.appendingPathComponent("analytics.sqlite3")
|
|
}
|
|
}
|
|
|
|
private struct ExtensionAnalyticsClock: AnalyticsWallClock {
|
|
func now() -> Date {
|
|
Date(timeIntervalSince1970: 1_700_000_000)
|
|
}
|
|
}
|
|
|
|
private struct ExtensionAnalyticsMonotonicClock: AnalyticsMonotonicClock {
|
|
func nowNanoseconds() -> UInt64 {
|
|
0
|
|
}
|
|
}
|
|
|
|
private final class ExtensionAnalyticsUUIDGenerator:
|
|
AnalyticsUUIDGenerating,
|
|
@unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var value = 1
|
|
|
|
func makeUUID() -> UUID {
|
|
lock.withLock {
|
|
defer { value += 1 }
|
|
return UUID(
|
|
uuidString: String(
|
|
format: "00000000-0000-0000-0000-%012x",
|
|
value
|
|
)
|
|
)!
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ExtensionAnalyticsRandomGenerator: AnalyticsRandomGenerating {
|
|
func next(upperBound: UInt64) -> UInt64 {
|
|
0
|
|
}
|
|
}
|
|
|
|
private actor ExtensionAnalyticsNetwork: AnalyticsNetworking {
|
|
private var captured: [AnalyticsHTTPRequest] = []
|
|
|
|
func send(_ request: AnalyticsHTTPRequest) async throws -> AnalyticsHTTPResponse {
|
|
captured.append(request)
|
|
let upload = try JSONDecoder().decode(
|
|
AnalyticsUploadRequest.self,
|
|
from: request.body
|
|
)
|
|
return AnalyticsHTTPResponse(
|
|
statusCode: 200,
|
|
headers: [:],
|
|
body: Data(
|
|
#"{"accepted":\#(upload.events.count),"replayed":0}"#.utf8
|
|
)
|
|
)
|
|
}
|
|
|
|
func requests() -> [AnalyticsHTTPRequest] {
|
|
captured
|
|
}
|
|
}
|