Skip to content

Commit

Permalink
Fix PaywallEvents failing to deserialize (#4520)
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro authored Nov 27, 2024
1 parent 671a415 commit fd4af48
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 37 deletions.
16 changes: 8 additions & 8 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@
4FD291BE2A1E9A2E0098D1B9 /* StoreKit2TransactionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD291BD2A1E9A2E0098D1B9 /* StoreKit2TransactionFetcherTests.swift */; };
4FD3688B2AA7C12600F63354 /* PaywallEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD3688A2AA7C12600F63354 /* PaywallEvent.swift */; };
4FD368B42AA7CFED00F63354 /* PaywallEventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD368B32AA7CFED00F63354 /* PaywallEventStore.swift */; };
4FD368B62AA7D09C00F63354 /* PaywallEventSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD368B52AA7D09C00F63354 /* PaywallEventSerializer.swift */; };
4FD368B62AA7D09C00F63354 /* StoredEventSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD368B52AA7D09C00F63354 /* StoredEventSerializer.swift */; };
4FD7E8662AABC4470055406F /* PurchasesPaywallEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD7E8652AABC4470055406F /* PurchasesPaywallEventsTests.swift */; };
4FDA13842A33D9BD00C45CFE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4FDA13662A33D13700C45CFE /* PrivacyInfo.xcprivacy */; };
4FDF10E72A725EA6004F3680 /* ExternalPurchasesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF10E62A725EA6004F3680 /* ExternalPurchasesManager.swift */; };
Expand All @@ -460,7 +460,7 @@
4FE0685F2A5F54C500B8F56C /* PackageTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE0685E2A5F54C500B8F56C /* PackageTypeTests.swift */; };
4FE6669F2A2F95A1004EEAFC /* PaywallExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6669E2A2F95A1004EEAFC /* PaywallExtensions.swift */; };
4FE6FEEA2AA940E300780B45 /* PaywallEventStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */; };
4FE6FEEB2AA940E300780B45 /* PaywallEventSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */; };
4FE6FEEB2AA940E300780B45 /* StoredEventSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE82AA940E300780B45 /* StoredEventSerializerTests.swift */; };
4FEF41AB2B4F2F3400CD699F /* MapAppStoreDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF41AA2B4F2F3400CD699F /* MapAppStoreDetector.swift */; };
4FEF41AD2B4F301800CD699F /* MacAppStoreDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF41AC2B4F301800CD699F /* MacAppStoreDetectorTests.swift */; };
4FF017C32AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF017C22AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift */; };
Expand Down Expand Up @@ -1702,7 +1702,7 @@
4FD291BD2A1E9A2E0098D1B9 /* StoreKit2TransactionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcherTests.swift; sourceTree = "<group>"; };
4FD3688A2AA7C12600F63354 /* PaywallEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEvent.swift; sourceTree = "<group>"; };
4FD368B32AA7CFED00F63354 /* PaywallEventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEventStore.swift; sourceTree = "<group>"; };
4FD368B52AA7D09C00F63354 /* PaywallEventSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEventSerializer.swift; sourceTree = "<group>"; };
4FD368B52AA7D09C00F63354 /* StoredEventSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredEventSerializer.swift; sourceTree = "<group>"; };
4FD7E8652AABC4470055406F /* PurchasesPaywallEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesPaywallEventsTests.swift; sourceTree = "<group>"; };
4FDA13662A33D13700C45CFE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4FDE95A92A769E9E006E7D2D /* XC-AllTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "XC-AllTests.xctestplan"; path = "Tests/TestPlans/XC-AllTests.xctestplan"; sourceTree = "<group>"; };
Expand All @@ -1720,7 +1720,7 @@
4FE6669E2A2F95A1004EEAFC /* PaywallExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallExtensions.swift; sourceTree = "<group>"; };
4FE6FEE42AA940B700780B45 /* StoredEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoredEvent.swift; sourceTree = "<group>"; };
4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventStoreTests.swift; sourceTree = "<group>"; };
4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventSerializerTests.swift; sourceTree = "<group>"; };
4FE6FEE82AA940E300780B45 /* StoredEventSerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoredEventSerializerTests.swift; sourceTree = "<group>"; };
4FED3AD62AAA7DD4001D4D5E /* purchases-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "purchases-ios"; path = ..; sourceTree = "<group>"; };
4FEF41AA2B4F2F3400CD699F /* MapAppStoreDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAppStoreDetector.swift; sourceTree = "<group>"; };
4FEF41AC2B4F301800CD699F /* MacAppStoreDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAppStoreDetectorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3549,6 +3549,7 @@
isa = PBXGroup;
children = (
4FE6FEE42AA940B700780B45 /* StoredEvent.swift */,
4FD368B52AA7D09C00F63354 /* StoredEventSerializer.swift */,
353DE0052CCA4EAE00A8F632 /* Networking */,
);
path = Events;
Expand Down Expand Up @@ -3967,7 +3968,6 @@
4FD3688A2AA7C12600F63354 /* PaywallEvent.swift */,
4FFFE6C32AA9464100B2955C /* PaywallEventsManager.swift */,
4FD368B32AA7CFED00F63354 /* PaywallEventStore.swift */,
4FD368B52AA7D09C00F63354 /* PaywallEventSerializer.swift */,
);
path = Events;
sourceTree = "<group>";
Expand All @@ -3994,7 +3994,7 @@
4FFCED812AA941B200118EF4 /* PaywallEventsBackendTests.swift */,
4FFFE6C72AA9467800B2955C /* PaywallEventsManagerTests.swift */,
4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */,
4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */,
4FE6FEE82AA940E300780B45 /* StoredEventSerializerTests.swift */,
4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */,
4FD7E8652AABC4470055406F /* PurchasesPaywallEventsTests.swift */,
);
Expand Down Expand Up @@ -5845,7 +5845,7 @@
F530E4FF275646EF001AF6BD /* MacDevice.swift in Sources */,
F5714EAC26D7A87B00635477 /* PurchaseOwnershipType+Extensions.swift in Sources */,
57CD86DA291C1E2300768DE1 /* UserDefaults+Extensions.swift in Sources */,
4FD368B62AA7D09C00F63354 /* PaywallEventSerializer.swift in Sources */,
4FD368B62AA7D09C00F63354 /* StoredEventSerializer.swift in Sources */,
4D6ABB0C2AF13F9400BB2A08 /* StoreKit2Receipt.swift in Sources */,
F5BE424026962ACF00254A30 /* ReceiptRefreshPolicy.swift in Sources */,
9A65E0762591977200DE00B0 /* IdentityStrings.swift in Sources */,
Expand Down Expand Up @@ -6042,7 +6042,7 @@
2DDF41CA24F6F4C3005BC22D /* ArraySlice_UInt8+ExtensionsTests.swift in Sources */,
2DDF41E124F6F527005BC22D /* MockReceiptParser.swift in Sources */,
57E415EB2846962500EA5460 /* PurchasesSyncPurchasesTests.swift in Sources */,
4FE6FEEB2AA940E300780B45 /* PaywallEventSerializerTests.swift in Sources */,
4FE6FEEB2AA940E300780B45 /* StoredEventSerializerTests.swift in Sources */,
5766AAC5283E843300FA6091 /* PurchasesConfiguringTests.swift in Sources */,
351B514D26D44A8600BD2BD7 /* MockHTTPClient.swift in Sources */,
4FBBD4E42A620539001CBA21 /* PaywallColorTests.swift in Sources */,
Expand Down
42 changes: 32 additions & 10 deletions Sources/Events/StoredEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ import Foundation
/// Contains the necessary information for storing and sending events.
struct StoredEvent {

private(set) var encodedEvent: AnyEncodable
private(set) var encodedEvent: String
private(set) var userID: String
private(set) var feature: Feature

init?<T: Encodable>(event: T, userID: String, feature: Feature) {
guard let data = try? JSONEncoder.default.encode(value: event),
let dictionary = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
guard let encodedJSON = try? event.encodedJSON else {
return nil
}

self.encodedEvent = AnyEncodable(dictionary)
self.encodedEvent = encodedJSON
self.userID = userID
self.feature = feature
}
Expand Down Expand Up @@ -56,7 +55,24 @@ extension StoredEvent: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.encodedEvent = try container.decode(AnyEncodable.self, forKey: .encodedEvent)
// Try to decode as string first (new format)
if let jsonString = try? container.decode(String.self, forKey: .encodedEvent) {
self.encodedEvent = jsonString
} else {
// Fall back to old format (direct dictionary)
if let oldEvent = try? container.decode(AnyEncodable.self, forKey: .encodedEvent),
let jsonString = try? oldEvent.encodedJSON {
self.encodedEvent = jsonString
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: [CodingKeys.encodedEvent],
debugDescription: "Could not convert old format to JSON string"
)
)
}
}

self.userID = try container.decode(String.self, forKey: .userID)
if let featureString = try container.decodeIfPresent(String.self, forKey: .feature),
let feature = Feature(rawValue: featureString) {
Expand All @@ -71,14 +87,20 @@ extension StoredEvent: Codable {
extension StoredEvent: Equatable {

static func == (lhs: StoredEvent, rhs: StoredEvent) -> Bool {
guard let lhsValue = lhs.encodedEvent.value as? [String: Any],
let rhsValue = rhs.encodedEvent.value as? [String: Any] else {
guard lhs.userID == rhs.userID,
lhs.feature == rhs.feature else {
return false
}

// Compare decoded events instead of raw JSON strings
guard let lhsData = lhs.encodedEvent.data(using: .utf8),
let rhsData = rhs.encodedEvent.data(using: .utf8),
let lhsDict = try? JSONSerialization.jsonObject(with: lhsData) as? [String: Any],
let rhsDict = try? JSONSerialization.jsonObject(with: rhsData) as? [String: Any] else {
return false
}

return lhs.userID == rhs.userID &&
lhs.feature == rhs.feature &&
(lhsValue as NSDictionary).isEqual(to: rhsValue)
return NSDictionary(dictionary: lhsDict).isEqual(rhsDict)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@
//
// https://opensource.org/licenses/MIT
//
// PaywallEventSerializer.swift
// StoredEventSerializer.swift
//
// Created by Nacho Soto on 9/5/23.

import Foundation

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
enum PaywallEventSerializer {
enum StoredEventSerializer {

private struct FailedEncodingEventError: Error {}

/// Encodes a `PaywallEvent` in a format suitable to be stored by `PaywallEventStore`.
/// Encodes a ``StoredEvent`` in a format suitable to be stored by `PaywallEventStore`.
static func encode(_ event: StoredEvent) throws -> String {
let data = try JSONEncoder.default.encode(value: event)

return try String(data: data, encoding: .utf8)
.orThrow(FailedEncodingEventError())
}

/// Decodes a `StoredEvent`.
/// Decodes a ``StoredEvent``.
static func decode(_ event: String) throws -> StoredEvent {
return try JSONDecoder.default.decode(jsonData: event.asData)
}
Expand Down
10 changes: 7 additions & 3 deletions Sources/Logging/Strings/PaywallsStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ enum PaywallsStrings {
case event_flush_starting(count: Int)
case event_sync_failed(Error)
case event_cannot_serialize
case event_cannot_deserialize
case event_cannot_get_encoded_event
case event_cannot_deserialize(Error)

}

Expand Down Expand Up @@ -109,8 +110,11 @@ extension PaywallsStrings: LogMessage {
case .event_cannot_serialize:
return "Couldn't serialize PaywallEvent to storage."

case .event_cannot_deserialize:
return "Couldn't deserialize PaywallEvent from storage."
case .event_cannot_get_encoded_event:
return "Couldn't get encoded event from storage."

case let .event_cannot_deserialize(error):
return "Couldn't deserialize PaywallEvent from storage. Error: \((error as NSError).description)"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ extension EventsRequest.PaywallEvent {

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
init?(storedEvent: StoredEvent) {
guard let eventData = storedEvent.encodedEvent.value as? [String: Any] else {
Logger.error(Strings.paywalls.event_cannot_deserialize)
guard let jsonData = storedEvent.encodedEvent.data(using: .utf8) else {
Logger.error(Strings.paywalls.event_cannot_get_encoded_event)
return nil
}

do {
let paywallEvent: PaywallEvent = try JSONDecoder.default.decode(dictionary: eventData)
let paywallEvent = try JSONDecoder.default.decode(PaywallEvent.self, from: jsonData)
let creationData = paywallEvent.creationData
let data = paywallEvent.data

Expand All @@ -68,6 +69,7 @@ extension EventsRequest.PaywallEvent {
localeIdentifier: data.localeIdentifier
)
} catch {
Logger.error(Strings.paywalls.event_cannot_deserialize(error))
return nil
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Paywalls/Events/PaywallEventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal actor PaywallEventStore: PaywallEventStoreType {
Logger.verbose(PaywallEventStoreStrings.storing_event_without_json)
}

let event = try PaywallEventSerializer.encode(storedEvent)
let event = try StoredEventSerializer.encode(storedEvent)
await self.handler.append(line: event)
} catch {
Logger.error(PaywallEventStoreStrings.error_storing_event(error))
Expand All @@ -59,7 +59,7 @@ internal actor PaywallEventStore: PaywallEventStoreType {
do {
return try await self.handler.readLines()
.prefix(count)
.compactMap { try? PaywallEventSerializer.decode($0) }
.compactMap { try? StoredEventSerializer.decode($0) }
.extractValues()
} catch {
Logger.error(PaywallEventStoreStrings.error_fetching_events(error))
Expand Down
2 changes: 1 addition & 1 deletion Sources/Paywalls/Events/PaywallEventsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ actor PaywallEventsManager: PaywallEventsManagerType {
}

func track(paywallEvent: PaywallEvent) async {
guard let event: StoredEvent = .init(event: AnyEncodable(paywallEvent),
guard let event: StoredEvent = .init(event: paywallEvent,
userID: self.userProvider.currentAppUserID,
feature: .paywalls) else {
Logger.error(Strings.paywalls.event_cannot_serialize)
Expand Down
29 changes: 28 additions & 1 deletion Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,33 @@ class PaywallEventsRequestTests: TestCase {
assertSnapshot(matching: requestEvent, as: .formattedJson)
}

func testCanInitFromDeserializedEvent() throws {
let expectedUserID = "test-user"
let paywallEventCreationData: PaywallEvent.CreationData = .init(
id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!,
date: .init(timeIntervalSince1970: 1694029328)
)
let paywallEventData: PaywallEvent.Data = .init(
offeringIdentifier: "offeringIdentifier",
paywallRevision: 0,
sessionID: .init(uuidString: "73616D70-6C65-2073-7472-696E67000000")!,
displayMode: .fullScreen,
localeIdentifier: "en_US",
darkMode: true
)
let paywallEvent = PaywallEvent.impression(paywallEventCreationData, paywallEventData)

let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, userID: expectedUserID, feature: .paywalls))
let serializedEvent = try StoredEventSerializer.encode(storedEvent)
let deserializedEvent = try StoredEventSerializer.decode(serializedEvent)
expect(deserializedEvent.userID) == expectedUserID
expect(deserializedEvent.feature) == .paywalls

let requestEvent = try XCTUnwrap(EventsRequest.PaywallEvent(storedEvent: deserializedEvent))

assertSnapshot(matching: requestEvent, as: .formattedJson)
}

// MARK: -

private static let eventCreationData: PaywallEvent.CreationData = .init(
Expand All @@ -66,7 +93,7 @@ class PaywallEventsRequestTests: TestCase {

private static let eventData: PaywallEvent.Data = .init(
offeringIdentifier: "offering",
paywallRevision: 5,
paywallRevision: 0,
sessionID: .init(uuidString: "98CC0F1D-7665-4093-9624-1D7308FFF4DB")!,
displayMode: .fullScreen,
localeIdentifier: "es_ES",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//
// https://opensource.org/licenses/MIT
//
// PaywallEventSerializerTests.swift
// StoredEventSerializerTests.swift
//
// Created by Nacho Soto on 9/5/23.

Expand All @@ -17,7 +17,7 @@ import Nimble
import XCTest

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
class PaywallEventSerializerTests: TestCase {
class StoredEventSerializerTests: TestCase {

override func setUpWithError() throws {
try super.setUpWithError()
Expand Down Expand Up @@ -52,6 +52,34 @@ class PaywallEventSerializerTests: TestCase {
expect(try event.encodeAndDecode()) == event
}

func testEncodingBooleans() throws {
let expectedUserID = "test-user"
let paywallEventCreationData: PaywallEvent.CreationData = .init(
id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!,
date: .init(timeIntervalSince1970: 1694029328)
)
let paywallEventData: PaywallEvent.Data = .init(
offeringIdentifier: "offeringIdentifier",
paywallRevision: 0,
sessionID: .init(uuidString: "73616D70-6C65-2073-7472-696E67000000")!,
displayMode: .fullScreen,
localeIdentifier: "en_US",
darkMode: true
)
let paywallEvent = PaywallEvent.impression(paywallEventCreationData, paywallEventData)

let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, userID: expectedUserID, feature: .paywalls))
let serializedEvent = try StoredEventSerializer.encode(storedEvent)
let deserializedEvent = try StoredEventSerializer.decode(serializedEvent)
expect(deserializedEvent.userID) == expectedUserID
expect(deserializedEvent.feature) == .paywalls

let eventData = deserializedEvent.encodedEvent
let jsonData = try XCTUnwrap(storedEvent.encodedEvent.data(using: .utf8))
let decodedPaywallEvent = try JSONDecoder.default.decode(PaywallEvent.self, from: jsonData)
expect(decodedPaywallEvent) == paywallEvent
}

// MARK: -

private static let userID = UUID().uuidString
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"app_user_id" : "test-user",
"dark_mode" : true,
"display_mode" : "full_screen",
"id" : "72164C05-2BDC-4807-8918-A4105F727DEB",
"locale" : "en_US",
"offering_id" : "offeringIdentifier",
"paywall_revision" : 0,
"session_id" : "73616D70-6C65-2073-7472-696E67000000",
"timestamp" : 1694029328000,
"type" : "paywall_impression",
"version" : 1
}
Loading

0 comments on commit fd4af48

Please sign in to comment.