// AnalyticsRepositoryTests.swift // OSGKeyboardTests // // Durable identity, queue policy, retention, and cross-process lease coverage. import Foundation @testable import OSGKeyboardShared import XCTest final class AnalyticsRepositoryTests: XCTestCase { func testFirstOpenMarkerAndEventAreCreatedExactlyOnceAcrossRestart() async throws { let url = try analyticsTemporaryDatabaseURL() let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .referral ) await repository.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .socialContent ) let firstSnapshot = await repository.debugSnapshot() XCTAssertTrue(firstSnapshot.firstOpenRecorded) XCTAssertEqual(firstSnapshot.pendingEvents.map(\.eventType), [.firstOpen]) let decodedEvents = try await analyticsDecodePendingEvents(repository: repository) let event = try XCTUnwrap(decodedEvents.first) XCTAssertEqual(event.clientEventId, analyticsTestUUID(2)) XCTAssertEqual(event.acquisitionChannel, .referral) let firstLeasedBatch = await repository.leaseBatch( ownerID: "first-open", configuration: AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")! ) ) let firstLease = try XCTUnwrap(firstLeasedBatch) XCTAssertEqual(firstLease.installationID, analyticsTestUUID(1)) await repository.releaseEvents(firstLease.events, leaseID: firstLease.leaseID) await repository.releaseGlobalLease(ownerID: "first-open") let restarted = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator(startingAt: 100) ) await restarted.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .appStoreOrganic ) let restartedSnapshot = await restarted.debugSnapshot() XCTAssertEqual(restartedSnapshot.installationID, analyticsTestUUID(1)) XCTAssertEqual(restartedSnapshot.pendingEvents.map(\.eventType), [.firstOpen]) } func testSessionWindowTracksThirtyMinutesOfInactivityPerSurface() async throws { let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL() ), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.recordSessionIfNeeded(context: analyticsTestContext) clock.advance(by: 29 * 60) await repository.recordSessionIfNeeded(context: analyticsTestContext) clock.advance(by: 29 * 60) await repository.recordSessionIfNeeded(context: analyticsTestContext) var snapshot = await repository.debugSnapshot() XCTAssertEqual(snapshot.pendingEvents.map(\.eventType), [.sessionStarted]) clock.advance(by: 30 * 60) await repository.recordSessionIfNeeded(context: analyticsTestContext) snapshot = await repository.debugSnapshot() XCTAssertEqual( snapshot.pendingEvents.compactMap(\.eventType), [.sessionStarted, .sessionStarted] ) } func testDisableClearsQueuePersistsAcrossRestartAndReenableRotatesIdentity() async throws { let url = try analyticsTemporaryDatabaseURL() let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .unknown ) let originalID = await repository.debugSnapshot().installationID await repository.setEnabled(false) var snapshot = await repository.debugSnapshot() XCTAssertFalse(snapshot.enabled) XCTAssertTrue(snapshot.pendingEvents.isEmpty) let restarted = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator(startingAt: 100) ) let remainsDisabled = await restarted.isEnabled() XCTAssertFalse(remainsDisabled) await restarted.record( eventType: .keyboardActivated, context: analyticsTestContext ) let disabledSnapshot = await restarted.debugSnapshot() XCTAssertTrue(disabledSnapshot.pendingEvents.isEmpty) await restarted.setEnabled(true) await restarted.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .referral ) await restarted.record( eventType: .keyboardActivated, context: analyticsTestContext ) snapshot = await restarted.debugSnapshot() XCTAssertTrue(snapshot.enabled) XCTAssertNotEqual(snapshot.installationID, originalID) XCTAssertTrue(snapshot.firstOpenRecorded) XCTAssertEqual(snapshot.pendingEvents.map(\.eventType), [.keyboardActivated]) } func testAccountObservationSwitchAndDeletionRotateWithoutRepeatingFirstOpen() async throws { let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL() ), clock: AnalyticsTestWallClock(), uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .appStoreOrganic ) let installationBeforeAccount = await repository.debugSnapshot().installationID let first = await repository.observeAccount(stableIdentifier: "account-a") guard case .firstAccount = first else { return XCTFail("Expected firstAccount, got \(first)") } let installationAfterFirstAccount = await repository.debugSnapshot().installationID XCTAssertEqual(installationAfterFirstAccount, installationBeforeAccount) let unchanged = await repository.observeAccount(stableIdentifier: "account-a") guard case .unchanged = unchanged else { return XCTFail("Expected unchanged, got \(unchanged)") } await repository.record( eventType: .keyboardActivated, context: analyticsTestContext ) let switched = await repository.observeAccount(stableIdentifier: "account-b") guard case .switchedAccount = switched else { return XCTFail("Expected switchedAccount, got \(switched)") } let afterSwitch = await repository.debugSnapshot() XCTAssertNotEqual(afterSwitch.installationID, installationBeforeAccount) XCTAssertTrue(afterSwitch.pendingEvents.isEmpty) XCTAssertTrue(afterSwitch.firstOpenRecorded) await repository.record( eventType: .purchaseViewed, context: analyticsTestContext ) let switchedID = afterSwitch.installationID await repository.handleAccountDeletion() let afterDeletion = await repository.debugSnapshot() XCTAssertNotEqual(afterDeletion.installationID, switchedID) XCTAssertTrue(afterDeletion.pendingEvents.isEmpty) XCTAssertTrue(afterDeletion.firstOpenRecorded) let firstAfterDeletion = await repository.observeAccount(stableIdentifier: "account-b") guard case .firstAccount = firstAfterDeletion else { return XCTFail("Expected firstAccount after deletion, got \(firstAfterDeletion)") } } func testRetryPreservesCanonicalPayloadAndClientEventID() async throws { let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL() ), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await analyticsRecordKeyboardEvents(count: 1, repository: repository) let snapshot = await repository.debugSnapshot() let rowID = try XCTUnwrap(snapshot.pendingEvents.first?.rowID) let storedOriginalPayload = await repository.debugPayloadBytes(rowID: rowID) let originalPayload = try XCTUnwrap(storedOriginalPayload) let originalEvent = try JSONDecoder().decode( AnalyticsEvent.self, from: originalPayload ) let upload = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")! ) let leasedFirstBatch = await repository.leaseBatch( ownerID: "owner", configuration: upload ) let firstLease = try XCTUnwrap(leasedFirstBatch) await repository.scheduleRetry( events: firstLease.events, leaseID: firstLease.leaseID, delay: 10 ) let storedRetriedPayload = await repository.debugPayloadBytes(rowID: rowID) let retriedPayload = try XCTUnwrap(storedRetriedPayload) XCTAssertEqual(retriedPayload, originalPayload) let retrySnapshot = await repository.debugSnapshot() let retryDiagnostic = try XCTUnwrap(retrySnapshot.pendingEvents.first) XCTAssertEqual(retryDiagnostic.attemptCount, 1) XCTAssertEqual(retryDiagnostic.nextAttemptAt, clock.now().addingTimeInterval(10)) clock.advance(by: 10) let leasedSecondBatch = await repository.leaseBatch( ownerID: "owner", configuration: upload ) let secondLease = try XCTUnwrap(leasedSecondBatch) XCTAssertEqual(secondLease.events.first?.payload, originalPayload) let retriedEvent = try JSONDecoder().decode( AnalyticsEvent.self, from: try XCTUnwrap(secondLease.events.first?.payload) ) XCTAssertEqual(retriedEvent.clientEventId, originalEvent.clientEventId) } func testConcurrentRepositoriesEnforceGlobalLeaseAndRecoverExpiredEventLease() async throws { let url = try analyticsTemporaryDatabaseURL() let clock = AnalyticsTestWallClock() let firstRepository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) let secondRepository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator(startingAt: 100) ) await analyticsRecordKeyboardEvents(count: 1, repository: firstRepository) let upload = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")!, globalLeaseDuration: 60, eventLeaseDuration: 60 ) async let first = firstRepository.leaseBatch( ownerID: "first-owner", configuration: upload ) async let second = secondRepository.leaseBatch( ownerID: "second-owner", configuration: upload ) let simultaneous = await (first, second) XCTAssertEqual([simultaneous.0, simultaneous.1].compactMap { $0 }.count, 1) clock.advance(by: 61) let recovered = await secondRepository.leaseBatch( ownerID: "recovery-owner", configuration: upload ) XCTAssertEqual(recovered?.events.count, 1) } func testCrashAfterServerAcceptanceBeforeDeleteReplaysOriginalBytes() async throws { let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL() ), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await analyticsRecordKeyboardEvents(count: 1, repository: repository) let configuration = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")!, globalLeaseDuration: 60, eventLeaseDuration: 60 ) let crashedLease = await repository.leaseBatch( ownerID: "crashed-owner", configuration: configuration ) let acceptedButNotDeleted = try XCTUnwrap(crashedLease) clock.advance(by: 61) let network = AnalyticsQueueNetwork([ .response(analyticsSuccessResponse(accepted: 0, replayed: 1)) ]) let coordinator = AnalyticsUploadCoordinator( repository: repository, configuration: configuration, network: network, clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator(startingAt: 500), random: AnalyticsTestRandomGenerator() ) await coordinator.uploadAvailableEvents() let requests = await network.requests() XCTAssertEqual(requests.single?.body, acceptedButNotDeleted.body) let completedSnapshot = await repository.debugSnapshot() XCTAssertTrue(completedSnapshot.pendingEvents.isEmpty) } func testLeaseNeverMixesInstallationsInOneBatch() async throws { let url = try analyticsTemporaryDatabaseURL() let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await analyticsRecordKeyboardEvents(count: 1, repository: repository) let secondEvent = try AnalyticsEvent( clientEventId: analyticsTestUUID(99), eventType: .keyboardActivated, occurredAt: clock.now(), surface: .keyboard, appVersion: "2.0.0", osVersion: "26.0" ) let secondPayload = try AnalyticsCanonicalJSON.encode(secondEvent) let database = try SQLiteDatabase(url: url, busyTimeoutMilliseconds: 2_000) try database.execute( """ INSERT INTO pending_events ( installation_id, client_event_id, event_type, occurred_at, surface, payload, payload_size, priority, attempt_count, next_attempt_at, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?) """, bindings: [ .text(analyticsTestUUID(2_000).uuidString.lowercased()), .text(secondEvent.clientEventId.uuidString.lowercased()), .text(secondEvent.eventType.rawValue), .double(secondEvent.occurredAt.timeIntervalSince1970), .text(secondEvent.surface.rawValue), .blob(secondPayload), .int64(Int64(secondPayload.count)), .double(clock.now().timeIntervalSince1970 + 1) ] ) let upload = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")! ) let firstLeasedBatch = await repository.leaseBatch( ownerID: "first-installation", configuration: upload ) let firstBatch = try XCTUnwrap(firstLeasedBatch) XCTAssertEqual(firstBatch.events.count, 1) let firstInstallation = firstBatch.installationID let completed = await repository.complete( rowIDs: firstBatch.events.map(\.rowID), leaseID: firstBatch.leaseID ) XCTAssertTrue(completed) await repository.releaseGlobalLease(ownerID: "first-installation") let secondLeasedBatch = await repository.leaseBatch( ownerID: "second-installation", configuration: upload ) let secondBatch = try XCTUnwrap(secondLeasedBatch) XCTAssertEqual(secondBatch.events.count, 1) XCTAssertNotEqual(secondBatch.installationID, firstInstallation) } func testBatchHonorsFiftyEventAndDynamicBodyByteLimits() async throws { let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL() ), clock: AnalyticsTestWallClock(), uuidGenerator: AnalyticsTestUUIDGenerator() ) await analyticsRecordKeyboardEvents(count: 55, repository: repository) let defaultUpload = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")!, maximumBatchCount: 100, maximumBodyBytes: 100_000 ) let countLease = await repository.leaseBatch( ownerID: "count", configuration: defaultUpload ) let countLimited = try XCTUnwrap(countLease) XCTAssertEqual(countLimited.events.count, 50) XCTAssertLessThanOrEqual(countLimited.body.count, 60 * 1_024) await repository.releaseEvents( countLimited.events, leaseID: countLimited.leaseID ) await repository.releaseGlobalLease(ownerID: "count") let byteUpload = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")!, maximumBatchCount: 50, maximumBodyBytes: 1_024 ) let byteLease = await repository.leaseBatch( ownerID: "bytes", configuration: byteUpload ) let byteLimited = try XCTUnwrap(byteLease) XCTAssertFalse(byteLimited.events.isEmpty) XCTAssertLessThan(byteLimited.events.count, 50) XCTAssertLessThanOrEqual(byteLimited.body.count, 1_024) let snapshot = await repository.debugSnapshot() let selectedIDs = Set(byteLimited.events.map(\.rowID)) let nextDiagnostic = try XCTUnwrap( snapshot.pendingEvents.first { !selectedIDs.contains($0.rowID) } ) let storedNextPayload = await repository.debugPayloadBytes( rowID: nextDiagnostic.rowID ) let nextPayload = try XCTUnwrap(storedNextPayload) XCTAssertGreaterThan(byteLimited.body.count + 1 + nextPayload.count, 1_024) } func testRetentionExpiresAfterThirtyFourDaysAndToleratesClockRollback() async throws { let retention = 34 * 24 * 60 * 60.0 let clock = AnalyticsTestWallClock() let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL(), eventRetention: retention ), clock: clock, uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.record( eventType: .keyboardActivated, context: analyticsTestContext ) clock.advance(by: retention) await repository.record( eventType: .purchaseViewed, context: analyticsTestContext ) let boundarySnapshot = await repository.debugSnapshot() XCTAssertEqual(boundarySnapshot.pendingEvents.count, 2) clock.advance(by: 0.001) await repository.record( eventType: .purchaseStarted, context: analyticsTestContext ) var events = try await analyticsDecodePendingEvents(repository: repository) XCTAssertEqual(Set(events.map(\.eventType)), [.purchaseViewed, .purchaseStarted]) let future = clock.now().addingTimeInterval(24 * 60 * 60) clock.set(future) await repository.record( eventType: .referralShared, context: analyticsTestContext ) clock.set(future.addingTimeInterval(-7 * 24 * 60 * 60)) let upload = AnalyticsUploadConfiguration( endpoint: URL(string: "https://analytics.test/events")! ) let batch = await repository.leaseBatch(ownerID: "rollback", configuration: upload) events = try batch?.events.map { try JSONDecoder().decode(AnalyticsEvent.self, from: $0.payload) } ?? [] XCTAssertTrue(events.contains(where: { $0.eventType == .referralShared })) } func testCapacityEvictsSessionAndKeyboardBeforeHighValueEvents() async throws { let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL(), maximumEventCount: 2 ), clock: AnalyticsTestWallClock(), uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .unknown ) await repository.recordSessionIfNeeded(context: analyticsTestContext) await repository.record( eventType: .keyboardActivated, context: analyticsTestContext ) var eventTypes = await repository.debugSnapshot().pendingEvents.compactMap(\.eventType) XCTAssertEqual(Set(eventTypes), [.firstOpen, .keyboardActivated]) await repository.record( eventType: .aiFeatureSucceeded, context: analyticsTestContext, dimensions: AnalyticsEventDimensions( feature: .polish, executionMode: .managed, durationBucket: .oneToThreeSeconds ) ) eventTypes = await repository.debugSnapshot().pendingEvents.compactMap(\.eventType) XCTAssertEqual(Set(eventTypes), [.firstOpen, .aiFeatureSucceeded]) } func testV1MigrationDropsIncompatibleQueuesAndRecreatesFirstOpen() async throws { let url = try analyticsTemporaryDatabaseURL() let installationID = analyticsTestUUID(42) try createAnalyticsV1Fixture( at: url, installationID: installationID ) let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration(databaseURL: url), clock: AnalyticsTestWallClock(), uuidGenerator: AnalyticsTestUUIDGenerator(startingAt: 100) ) let migrated = await repository.debugSnapshot() XCTAssertTrue(migrated.isAvailable) XCTAssertEqual(migrated.installationID, installationID) XCTAssertFalse(migrated.firstOpenRecorded) XCTAssertTrue(migrated.pendingEvents.isEmpty) XCTAssertTrue(migrated.quarantinedEvents.isEmpty) await repository.prepare( using: analyticsTestContext, firstOpenAcquisitionChannel: .appStoreOrganic ) let prepared = await repository.debugSnapshot() XCTAssertTrue(prepared.firstOpenRecorded) XCTAssertEqual(prepared.pendingEvents.map(\.eventType), [.firstOpen]) let events = try await analyticsDecodePendingEvents(repository: repository) XCTAssertEqual(events.single?.acquisitionChannel, .appStoreOrganic) let database = try SQLiteDatabase(url: url, busyTimeoutMilliseconds: 2_000) XCTAssertEqual(try database.scalarInt64("PRAGMA user_version"), 2) let pendingColumns = try database.query("PRAGMA table_info(pending_events)") .compactMap { $0.text(at: 1) } let quarantinedColumns = try database.query("PRAGMA table_info(quarantined_events)") .compactMap { $0.text(at: 1) } XCTAssertTrue(pendingColumns.contains("installation_id")) XCTAssertTrue(quarantinedColumns.contains("installation_id")) } func testSuspendedDatabaseDropsRecordsUntilExplicitResume() async throws { let repository = AnalyticsRepository( configuration: AnalyticsRepositoryConfiguration( databaseURL: try analyticsTemporaryDatabaseURL() ), clock: AnalyticsTestWallClock(), uuidGenerator: AnalyticsTestUUIDGenerator() ) await repository.record( eventType: .keyboardActivated, context: analyticsTestContext ) await repository.suspendDatabaseAccess() await repository.record( eventType: .keyboardActivated, context: analyticsTestContext ) let suspendedSnapshot = await repository.debugSnapshot() XCTAssertFalse(suspendedSnapshot.isAvailable) await repository.resumeDatabaseAccess() await repository.record( eventType: .keyboardActivated, context: analyticsTestContext ) let resumedSnapshot = await repository.debugSnapshot() XCTAssertTrue(resumedSnapshot.isAvailable) XCTAssertEqual(resumedSnapshot.pendingEvents.count, 2) } } private extension Array { var single: Element? { count == 1 ? first : nil } } private func createAnalyticsV1Fixture( at url: URL, installationID: UUID ) throws { let database = try SQLiteDatabase(url: url, busyTimeoutMilliseconds: 2_000) try database.immediateTransaction { try database.execute( """ CREATE TABLE metadata ( key TEXT PRIMARY KEY NOT NULL, value BLOB NOT NULL ) WITHOUT ROWID """ ) try database.execute( """ CREATE TABLE 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, 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 TABLE 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 ) """ ) for (key, value) in [ ("enabled", "1"), ("installationId", installationID.uuidString.lowercased()), ("firstOpenRecorded", "1"), ("uploadLeaseOwner", "legacy-owner"), ("uploadLeaseExpiresAt", "9999999999") ] { try database.execute( "INSERT INTO metadata(key, value) VALUES(?, ?)", bindings: [.text(key), .text(value)] ) } let legacyPayload = Data( #"{"installationId":"legacy-must-not-upload","clientEventId":"old"}"#.utf8 ) try database.execute( """ INSERT INTO pending_events ( client_event_id, event_type, occurred_at, surface, payload, payload_size, priority, attempt_count, next_attempt_at, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, ?) """, bindings: [ .text(analyticsTestUUID(1).uuidString.lowercased()), .text(AnalyticsEventType.firstOpen.rawValue), .double(1_700_000_000), .text(AnalyticsSurface.app.rawValue), .blob(legacyPayload), .int64(Int64(legacyPayload.count)), .int64(2), .double(1_700_000_000) ] ) try database.execute( """ INSERT INTO quarantined_events ( client_event_id, event_type, occurred_at, surface, payload, payload_size, attempt_count, reason, quarantined_at ) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?) """, bindings: [ .text(analyticsTestUUID(2).uuidString.lowercased()), .text(AnalyticsEventType.keyboardActivated.rawValue), .double(1_700_000_000), .text(AnalyticsSurface.keyboard.rawValue), .blob(legacyPayload), .int64(Int64(legacyPayload.count)), .text("legacy"), .double(1_700_000_001) ] ) try database.execute("PRAGMA user_version = 1") } }