Closed alexstanbury closed 1 year ago
Further to this, I saw https://github.com/RevenueCat/purchases-ios/issues/499 and tried uninstalling the app. To my surprise, it worked as expected after reinstalling and testing. I had run into the linked issue where I was trying to use numbers in the data originally, but weirdly some of the attributes being ignored were never set to numbers.
Unfortunately, on the second run of the app (without reinstalling) the attributes are being ignored again, only this time it only kept 3 of them...
2023-09-08 12:55:23.367682+0100 App[1713:465539] [attribution] DEBUG: ℹ️ setting values for attributes: ["cost", "tmId", "accessId", "userId", "interval", "iapName", "teamId", "currencyCode", "teamName"]
2023-09-08 12:55:23.368099+0100 App[1713:465539] [attribution] DEBUG: ℹ️ Attribute set locally: [SubscriberAttribute] key: cost value: 120 setTime: 2023-09-08 11:55:23 +0000. It will be synced to the backend when the app backgrounds/foregrounds or when a purchase is made.
2023-09-08 12:55:23.370107+0100 App[1713:465539] [attribution] DEBUG: ℹ️ Attribute set locally: [SubscriberAttribute] key: interval value: year setTime: 2023-09-08 11:55:23 +0000. It will be synced to the backend when the app backgrounds/foregrounds or when a purchase is made.
2023-09-08 12:55:23.372435+0100 App[1713:465539] [attribution] DEBUG: ℹ️ Attribute set locally: [SubscriberAttribute] key: iapName value: MY_IAP_NAME_HERE setTime: 2023-09-08 11:55:23 +0000. It will be synced to the backend when the app backgrounds/foregrounds or when a purchase is made.
⚡️ TO JS undefined
2023-09-08 12:55:23.376695+0100 App[1713:465539] [offering] DEBUG: ℹ️ Skipping products request for these products because they were already cached:```
I've dug into the swift code, and it looks like there is some issue with caching, I'm not going to go any further but this is where it falls down:
func storeAttributeLocallyIfNeeded(key: String, value: String?, appUserID: String) {
let currentValue = currentValueForAttribute(key: key, appUserID: appUserID)
print("currentValue", currentValue as Any);
print("test", currentValue != (value ?? ""));
if currentValue == nil || currentValue != (value ?? "") {
print("do i get here?", currentValue as Any);
storeAttributeLocally(key: key, value: value ?? "", appUserID: appUserID)
}
}
The test prints false, currentValueForAttribute
seems to rely on some kind of cache, so it looks like something isn't being cleaned up properly after the attributes have successfully synced.
Hi @alexstanbury, just a theory but is it possible the property didn't change value from the original value set in the cache? If so, we would only update the value if it has changed. Also, could you confirm you get the values in the RevenueCat dashboard for your user?
I also noticed the typing of this function seems to be incorrect, it does not appear to return a promise.
Could you add more details about this? The current setAttributes
function is returning a Promise<void>
: https://github.com/RevenueCat/purchases-capacitor/blob/6.0.0-beta.2/src/definitions.ts#L453
You're right, it looks as though that is indeed the problem with the current situation - I think/hope the sequence of events that caused the problem is as follows:
Used setAttributes but with some of the attribute values not cast as strings (they were number). All of the attributes were added to the cache before any validation or remote syncing took place. Attributes were processed in sequence, when the first attribute that wasn't a string was encountered, the operation failed. The attributes that were placed after the problem attribute(s) in the sequence never got synced but remained in the cache. I Checked UI and API and there were attributes missing. These attributes remained in the cache and they don't change, meaning subsequent attempts to add them (even after casting to string) constantly failed because they were in the cache. Uninstalling app cleared the cache, meaning after reinstall they all got submitted as expected. Further to this, only attributes that change get submitted, as designed.
I think this issue could have been avoided by the attributes being parsed immediately when the call to setAttributes is made and failing early with a thrown error if they are not in the right format, before any caching takes place.
Regarding the promise, I am using a pretty dated setup, AngularJS with no Typescript, so I have to use the following syntax:
window.Capacitor.Plugins.Purchases.setAttributes({attributes:{test:'hello'}});
If I call it as a promise like:
window.Capacitor.Plugins.Purchases.setAttributes({attributes:{test:'hello'}}).then(function () {console.log("promise resolved")})
I get the error:
TypeError: window.Capacitor.Plugins.Purchases.setAttributes({test:'hello'}).then is not a function
However, I can call:
window.Capacitor.Plugins.Purchases.purchaseStoreProduct({ product: MY_PRODUCT_HERE }).then(function (purchase) {
//stuff here
}).catch(function (error) {
//stuff here
}).finally(function () {
//stuff here
})
And have no problems.
I think this issue could have been avoided by the attributes being parsed immediately when the call to setAttributes is made and failing early with a thrown error if they are not in the right format, before any caching takes place.
It's tricky to perform this type of runtime type checking in a capacitor plugin since the plugin jumps directly to the Android/iOS counterparts so we can't perform the type check in typescript/javascript. But we will study our options for this.
TypeError: window.Capacitor.Plugins.Purchases.setAttributes({test:'hello'}).then is not a function
That's interesting... Looking at the generated javascript, I'm not sure why it wouldn't recognize this as a promise but it would for purchaseStoreProduct
. One thing you could try is changing your code from:
window.Capacitor.Plugins.Purchases.setAttributes({attributes:{test:'hello'}}).then(function () {console.log("promise resolved")})
to
window.Capacitor.Plugins.Purchases.setAttributes({'test':'hello'}).then(function () {console.log("promise resolved")})
Though I'm not sure that's the cause of this issue...
@tonidero i can help you if you need for this, there is a way to make the TS/JS code of the plugin less dumb :) There no doc, but some good exemple like: https://github.com/transistorsoft/capacitor-background-geolocation/blob/master/src/index.ts
Hey @riderx, thanks for that! After taking a look, seems like that solution could indeed help us adding some runtime checks for the types. However, it would also mean we need to handle calling the native methods manually ourselves. We will consider it though, since I don't think we have many options. Thanks again!
@riderx thanks for the suggestion! 🙌
I'd like to figure out the exact source of the issue before taking action.
We have a suspicion that it might be related to Objective-C's dynamic typing for the dictionary of attributes that gets passed in to setAttributes
: it might just be getting sent as the wrong type, and not caught in the intermediate layer.
We're going to try to reproduce based on that theory and add more info once we've dug in
Hi @alexstanbury, after testing what happens when passing the wrong types when using Javascript, it seems that we currently set the value to an empty string, as indicated here: https://github.com/RevenueCat/purchases-hybrid-common/blob/main/ios/PurchasesHybridCommon/PurchasesHybridCommon/CommonFunctionality.swift#L448. I've also tested it and, after setting it a value with the wrong type (which gets set as an empty string in the cache), I can set a string value and the value is updated correctly according to my tests.
Does that match what you were experiencing?
I'm going to close this since it's been inactive but feel free to reopen!
I'm having a real head scratching issue with the setAttributes functionality, where randomly some attributes seem to be ignored.
They are all set to strings before being passed into
setAttributes
and are also stripped of any special characters.It seems to be completely random, no consistency as to what causes an attribute to be ignored, which is leading me to believe there may be some kind of race condition somewhere, although having looked through the code I can't see anything obvious.
This may be a core iOS SDK issue and I'll post over there too.
As my logs below show, there are 9 attributes passed in, but only 4 get used:
After backgrounding and then foregrounding the app, these are the logs:
I also noticed the typing of this function seems to be incorrect, it does not appear to return a promise.