Expand onboarding and adaptive keyboard intelligence

Add resilient usage analytics, OOBE gateway flows, clipboard semantic ranking, purchase recovery, style learning, and managed current-information search.
This commit is contained in:
Rocky
2026-08-22 16:33:18 +08:00
parent ac374631ae
commit e5a83843db
162 changed files with 23574 additions and 1013 deletions
@@ -72,6 +72,7 @@ struct AnalyticsLeasedEvent: Sendable {
struct AnalyticsLeasedBatch: Sendable {
let leaseID: String
let installationID: UUID
let events: [AnalyticsLeasedEvent]
let body: Data
}
@@ -141,7 +142,6 @@ public actor AnalyticsRepository {
return
}
let event = try AnalyticsEvent(
installationId: installationID,
clientEventId: uuidGenerator.makeUUID(),
eventType: .firstOpen,
occurredAt: clock.now(),
@@ -150,7 +150,11 @@ public actor AnalyticsRepository {
osVersion: context.environment.osVersion,
acquisitionChannel: firstOpenAcquisitionChannel
)
try insert(event, database: database)
try insert(
event,
installationID: installationID,
database: database
)
try setMetadata(
"1",
for: MetadataKey.firstOpenRecorded,
@@ -172,6 +176,21 @@ public actor AnalyticsRepository {
}
}
/// Returns the existing App Group installation UUID, creating it only while
/// analytics is enabled. Aggregate keyboard usage reuses this identity.
public func installationIdentifierIfEnabled() -> UUID? {
guard let database = openDatabaseIfNeeded() else { return nil }
do {
return try database.immediateTransaction {
_ = try ensureEnabled(database)
guard try isEnabled(database) else { return nil }
return try ensureInstallation(database)
}
} catch {
return nil
}
}
/// Read-only diagnostics for tests and support tooling. Event payload content
/// is deliberately excluded from this public snapshot.
public func debugSnapshot() -> AnalyticsRepositoryDebugSnapshot {
@@ -355,7 +374,6 @@ public actor AnalyticsRepository {
guard try isEnabled(database) else { return }
let installationID = try ensureInstallation(database)
let event = try AnalyticsEvent(
installationId: installationID,
clientEventId: uuidGenerator.makeUUID(),
eventType: eventType,
occurredAt: clock.now(),
@@ -368,7 +386,11 @@ public actor AnalyticsRepository {
failureCategory: dimensions.failureCategory,
durationBucket: dimensions.durationBucket
)
try insert(event, database: database)
try insert(
event,
installationID: installationID,
database: database
)
try enforceStoragePolicy(database)
}
}
@@ -402,7 +424,6 @@ public actor AnalyticsRepository {
let installationID = try ensureInstallation(database)
let event = try AnalyticsEvent(
installationId: installationID,
clientEventId: uuidGenerator.makeUUID(),
eventType: .sessionStarted,
occurredAt: now,
@@ -410,7 +431,11 @@ public actor AnalyticsRepository {
appVersion: context.environment.appVersion,
osVersion: context.environment.osVersion
)
try insert(event, database: database)
try insert(
event,
installationID: installationID,
database: database
)
try setMetadata(
String(now.timeIntervalSince1970),
for: key,
@@ -441,19 +466,42 @@ public actor AnalyticsRepository {
try releaseExpiredEventLeases(now: now, database: database)
try deleteExpiredEvents(now: now, database: database)
guard let installationText = try database.query(
"""
SELECT installation_id
FROM pending_events
WHERE next_attempt_at <= ? AND lease_id IS NULL
ORDER BY priority DESC, created_at ASC
LIMIT 1
""",
bindings: [.double(now)]
).first?.text(at: 0),
let installationID = UUID(uuidString: installationText) else {
try clearUploadLease(database)
return nil
}
let candidates = try database.query(
"""
SELECT id, payload, attempt_count
FROM pending_events
WHERE next_attempt_at <= ? AND lease_id IS NULL
WHERE installation_id = ?
AND next_attempt_at <= ?
AND lease_id IS NULL
ORDER BY priority DESC, created_at ASC
LIMIT ?
""",
bindings: [.double(now), .int64(Int64(upload.maximumBatchCount))]
bindings: [
.text(installationText),
.double(now),
.int64(Int64(upload.maximumBatchCount))
]
)
var selected: [AnalyticsLeasedEvent] = []
var bodySize = Self.emptyRequestBody.count
var bodySize = Self.emptyRequestBody(
installationID: installationID
).count
for row in candidates {
guard let rowID = row.int64(at: 0),
let payload = row.data(at: 1),
@@ -504,8 +552,12 @@ public actor AnalyticsRepository {
}
return AnalyticsLeasedBatch(
leaseID: leaseID,
installationID: installationID,
events: selected,
body: Self.requestBody(for: selected)
body: Self.requestBody(
for: selected,
installationID: installationID
)
)
}
} catch {
@@ -696,18 +748,20 @@ public actor AnalyticsRepository {
private func insert(
_ event: AnalyticsEvent,
installationID: UUID,
database: SQLiteDatabase
) throws {
let payload = try AnalyticsCanonicalJSON.encode(event)
try database.execute(
"""
INSERT OR IGNORE INTO pending_events (
client_event_id, event_type, occurred_at, surface, payload,
payload_size, priority, lease_id, lease_expires_at,
installation_id, client_event_id, event_type, occurred_at,
surface, payload, payload_size, priority, lease_id, lease_expires_at,
attempt_count, next_attempt_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, 0, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, 0, ?)
""",
bindings: [
.text(installationID.uuidString.lowercased()),
.text(event.clientEventId.uuidString.lowercased()),
.text(event.eventType.rawValue),
.double(event.occurredAt.timeIntervalSince1970),
@@ -887,11 +941,12 @@ public actor AnalyticsRepository {
try database.execute(
"""
INSERT INTO quarantined_events (
client_event_id, event_type, occurred_at, surface, payload,
payload_size, attempt_count, reason, quarantined_at
installation_id, client_event_id, event_type, occurred_at,
surface, payload, payload_size, attempt_count, reason,
quarantined_at
)
SELECT client_event_id, event_type, occurred_at, surface, payload,
payload_size, attempt_count, ?, ?
SELECT installation_id, client_event_id, event_type, occurred_at,
surface, payload, payload_size, attempt_count, ?, ?
FROM pending_events
WHERE id = ?\(leaseClause)
""",
@@ -907,10 +962,18 @@ public actor AnalyticsRepository {
try enforceStoragePolicy(database)
}
private static let emptyRequestBody = Data(#"{"events":[]}"#.utf8)
private static func emptyRequestBody(installationID: UUID) -> Data {
requestBody(for: [], installationID: installationID)
}
private static func requestBody(for events: [AnalyticsLeasedEvent]) -> Data {
var body = Data(#"{"events":["#.utf8)
private static func requestBody(
for events: [AnalyticsLeasedEvent],
installationID: UUID
) -> Data {
var body = Data(
#"{"installationId":"\#(installationID.uuidString.lowercased())","events":["#
.utf8
)
for index in events.indices {
if index > 0 {
body.append(UInt8(ascii: ","))
@@ -967,7 +1030,7 @@ public actor AnalyticsRepository {
private func migrate(_ database: SQLiteDatabase) throws {
let version = try database.scalarInt64("PRAGMA user_version") ?? 0
guard version <= 1 else {
guard version <= 2 else {
throw SQLiteStoreError(
code: SQLITE_MISMATCH,
category: .schema,
@@ -984,58 +1047,90 @@ public actor AnalyticsRepository {
) WITHOUT ROWID
"""
)
try createCurrentQueueSchema(database)
try database.execute("PRAGMA user_version = 2")
}
} else if version == 1 {
try database.immediateTransaction {
// Another process may have completed the migration while this
// connection waited for BEGIN IMMEDIATE.
guard try database.scalarInt64("PRAGMA user_version") == 1 else {
return
}
// v1 payloads contain installationId inside each event and are
// incompatible with the server's strict v2 envelope. Drop them
// without decoding so neither old content nor identifiers can
// escape through logs or a partially migrated retry.
try database.execute("DROP TABLE IF EXISTS pending_events")
try database.execute("DROP TABLE IF EXISTS quarantined_events")
try createCurrentQueueSchema(database)
try database.execute(
"""
CREATE TABLE IF NOT EXISTS pending_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_event_id TEXT UNIQUE NOT NULL,
event_type TEXT NOT NULL,
occurred_at REAL NOT NULL,
surface TEXT NOT NULL,
payload BLOB NOT NULL,
payload_size INTEGER NOT NULL CHECK(payload_size >= 0),
priority INTEGER NOT NULL,
lease_id TEXT,
lease_expires_at REAL,
attempt_count INTEGER NOT NULL DEFAULT 0,
next_attempt_at REAL NOT NULL DEFAULT 0,
created_at REAL NOT NULL
)
"""
"DELETE FROM metadata WHERE key IN (?, ?, ?)",
bindings: [
.text(MetadataKey.firstOpenRecorded),
.text(MetadataKey.uploadLeaseOwner),
.text(MetadataKey.uploadLeaseExpiresAt)
]
)
try database.execute(
"""
CREATE INDEX IF NOT EXISTS pending_events_ready
ON pending_events(next_attempt_at, lease_id, priority, created_at)
"""
)
try database.execute(
"""
CREATE INDEX IF NOT EXISTS pending_events_expiry
ON pending_events(occurred_at)
"""
)
try database.execute(
"""
CREATE TABLE IF NOT EXISTS quarantined_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_event_id TEXT UNIQUE NOT NULL,
event_type TEXT NOT NULL,
occurred_at REAL NOT NULL,
surface TEXT NOT NULL,
payload BLOB NOT NULL,
payload_size INTEGER NOT NULL,
attempt_count INTEGER NOT NULL,
reason TEXT NOT NULL,
quarantined_at REAL NOT NULL
)
"""
)
try database.execute("PRAGMA user_version = 1")
try database.execute("PRAGMA user_version = 2")
}
}
}
private func createCurrentQueueSchema(_ database: SQLiteDatabase) throws {
try database.execute(
"""
CREATE TABLE IF NOT EXISTS pending_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
installation_id TEXT NOT NULL,
client_event_id TEXT UNIQUE NOT NULL,
event_type TEXT NOT NULL,
occurred_at REAL NOT NULL,
surface TEXT NOT NULL,
payload BLOB NOT NULL,
payload_size INTEGER NOT NULL CHECK(payload_size >= 0),
priority INTEGER NOT NULL,
lease_id TEXT,
lease_expires_at REAL,
attempt_count INTEGER NOT NULL DEFAULT 0,
next_attempt_at REAL NOT NULL DEFAULT 0,
created_at REAL NOT NULL
)
"""
)
try database.execute(
"""
CREATE INDEX IF NOT EXISTS pending_events_ready
ON pending_events(
installation_id, next_attempt_at, lease_id, priority, created_at
)
"""
)
try database.execute(
"""
CREATE INDEX IF NOT EXISTS pending_events_expiry
ON pending_events(occurred_at)
"""
)
try database.execute(
"""
CREATE TABLE IF NOT EXISTS quarantined_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
installation_id TEXT NOT NULL,
client_event_id TEXT UNIQUE NOT NULL,
event_type TEXT NOT NULL,
occurred_at REAL NOT NULL,
surface TEXT NOT NULL,
payload BLOB NOT NULL,
payload_size INTEGER NOT NULL,
attempt_count INTEGER NOT NULL,
reason TEXT NOT NULL,
quarantined_at REAL NOT NULL
)
"""
)
}
private func applyFileProtection(to databaseURL: URL) {
#if os(iOS)
for url in [
@@ -1161,7 +1256,7 @@ private struct SQLiteStoreError: Error, Sendable {
let operation: String
}
private enum SQLiteBinding {
enum SQLiteBinding {
case null
case int64(Int64)
case double(Double)
@@ -1169,7 +1264,7 @@ private enum SQLiteBinding {
case blob(Data)
}
private struct SQLiteRow {
struct SQLiteRow {
private let values: [SQLiteValue]
init(statement: OpaquePointer) {
@@ -1243,7 +1338,7 @@ private enum SQLiteValue {
case blob(Data)
}
private final class SQLiteDatabase {
final class SQLiteDatabase {
private static let transient = unsafeBitCast(
-1,
to: sqlite3_destructor_type.self