-
Notifications
You must be signed in to change notification settings - Fork 6
Improve how ParselyTracker
is accessed
#105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
To clearly distinguish initialization method from getting tracker instance method.
And move all methods invocations to static `ParselyTracker`. Throw exception if using `ParselyTracker` methods without prior `init`
As interface is no longer needed
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #105 +/- ##
===========================================
+ Coverage 57.60% 69.30% +11.70%
===========================================
Files 19 21 +2
Lines 401 404 +3
Branches 48 49 +1
===========================================
+ Hits 231 280 +49
+ Misses 157 110 -47
- Partials 13 14 +1 ☔ View full report in Codecov by Sentry. |
parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
Outdated
Show resolved
Hide resolved
@@ -24,76 +25,94 @@ import android.content.Context | |||
* Accessed as a singleton. Maintains a queue of pageview events in memory and periodically | |||
* flushes the queue to the Parse.ly pixel proxy server. | |||
*/ | |||
public interface ParselyTracker { | |||
public object ParselyTracker { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've decided to propose API change from usages like
ParselyTracker.sharedInstance().trackPageview()
to
ParselyTracker.trackPageview()
I think the second API is more expected (looking at API designs of e.g. Sentry or Crashlytics). To address possible null
when calling SDK methods before init
method, I've decided to propose "fail fast" approach of throwing our custom exception which clearly points the user to the problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this approach should work well - but I am not sure why we need to give up on the interface. Shouldn't we keep the previous interface and implement it for this object to make mocking possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure why we need to give up on the interface
I realized that the interface won't be needed anymore. Looking from the API consumer perspective, I can't find a good justification for it. If it'll be needed in the future for internal usage, we can always revert it. But for now, I would not introduce it to keep things simpler. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the consumer wants to write a unit test, wouldn't an interface be necessary to mock the tracker? I could totally be missing something - as we discussed - I don't do much active Kotlin development and maybe it's possible to mock it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure the interface will be possible with such a design of invoking methods statically. Methods declared in the interface would have to be either @JvmStatic
or not.
With @JvmStatic
If we had an interface like in this POC commit: f4a618e then we can't call method as member:
making this still untestable.
Without @JvmStatic
Alternatively, if we remove @JvmStatic
, then we can call method as member function, but can't as static method.
I'd like to keep static calls for the convenience of the clients and reduce the overhead of migration, even if it makes this part of the code untestable.
The alternative for the clients would be to prepare a wrapper - just like we do in other projects e.g. here.
WDYT? We can always revert to some variant of sharedInstance
method, but I'm not sure if it's worth it. E.g. Sentry Java SDK, SDK with some simillarities to this one, doesn't provide an interface to its entrypoint (link).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I don't think POC from f4a618e is possible at all: there's declaration clash between the interface and its companion object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we have the old design with the interface and have the new static objects on top as a wrapper? The wrapper could simply call the ParselyTracker.sharedInstance()
underneath? I didn't give this a try though, so hopefully I am not missing anything.
As nice as it's to be able to call ParselyTracker.trackPageview
directly, I feel like the benefit here is purely cosmetic. On the other hand, using an interface design has the benefit of making it mockable and unit testable by consumers. So, for me, the interface design is the clear winner.
I think we could do both, as suggested at the top of this comment - but if it was up to me, I'd only go with the interface design and not add the static object at all because having multiple APIs could be confusing.
Having said all that, if you want to go ahead with the static object design, I respect that decision and I can review the PR as is. Also, if you want a third opinion, feel free to ping other developers as well.
P.S: I think it's a great idea to look at other 3rd party SDKs, but I personally never liked Sentry's approach. I could totally be the odd man out, so please take that possibility into consideration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, I've brought back the interface and sharedInstance
method in fa7a23e
I agree, the benefit would be only cosmetic and would also cause more rework on client side. I also agree that a mockable interface is a nice thing to have for consumers. I hardly remember an Android SDK providing such, I'm not sure why, though. Or maybe I just missed it.
I'd only go with the interface design and not add the static object at all because having multiple APIs could be confusing.
I'm afraid, lack of static object will cause significant refactoring effort for the consumers because of the need of passing ParselyTracker
object. I can imagine this API used in places like e.g. Android Services where it's sometimes not trivial to pass an object directly. Also, I think usually logging SDK I've seen have static APIs: Crashlytics
, android.util.Log
, Log4j
So, to sum up, with the proposal introduced in: fa7a23e the SDK will have 2 ways of using it:
Via interface
This usage allows for unit testing SomeClass
by mocking ParselyTracker
interface
fun onCreate(...) {
ParselyTracker.init(...)
val tracker = ParselyTracker.sharedInstance()
val someClass = SomeClass(tracker)
someClass.openArticle()
}
class SomeClass(val tracker: ParselyTracker) {
fun openArticle() {
...
tracker.startEngagement()
...
}
}
Via static object
This makes SomeClass
more difficult to test, but the API easier to use and backward compatible
fun onCreate(...) {
ParselyTracker.init(...)
val someClass = SomeClass()
someClass.openArticle()
}
class SomeClass() {
fun openArticle() {
...
ParselyTracker.sharedInstance().startEngagement()
...
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the Via static object
would use ParselyTracker.startEngagement()
instead of ParselyTracker.sharedInstance().startEngagement()
?
Also, I think it'll be safe to mix and match the usages, could you confirm that?
It's not ideal to have 2 different ways to achieve the same thing, but I understand the arguments for it. If you believe this is the way to go, I am good with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the Via static object would use ParselyTracker.startEngagement() instead of ParselyTracker.sharedInstance().startEngagement()?
No, ParselyTracker.startEngagement()
will not work anymore (after the recent commit: fa7a23e). To use SDK statically, one would need to use ParselyTracker.sharedInstance().startEngagement()
now.
I see that the name of this approach is confusing, sorry. What I meant was "via statically exposed object" and this object is ParselyTrackerInternal
(hidden behind ParselyTracker
interface).
Also, I think it'll be safe to mix and match the usages, could you confirm that?
It'll be safe to mix usages - the ParselyTracker
will make sure that there's only one instance of the ParselyTrackerInternal
ever created.
hi @oguzkocer 👋 Here's my proposal to address comments from the last review. WDYT about them? |
parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt
Outdated
Show resolved
Hide resolved
parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
Outdated
Show resolved
Hide resolved
It's `private` method, not exposed to the client so there's no point in adding this annotation. Also, I don't add it to public methods because I don't want force clients to handle exception each time they want to call SDK.
The `ParselyTracker#tearDown` method was added as singleton persisted between unit tests, causing some of them to fail. I could introduce a similar behavior using reflection, but I believe `internal tearDown` method is cleaner and is not problematic as not exposed to the client
To align with other exception and move responsibility from `ParselyTracker`
parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
Outdated
Show resolved
Hide resolved
And `ParselyTracker#sharedInstance` method. Allows consumer clients to mock the `ParselyTracker` interface in their tests.
@wzieba Is this PR a blocker for you? Can I review it again on Friday? I am trying to finish something until EOD today, so if this is not a blocker for you, I can pay more attention to the review on Friday. |
@oguzkocer no worries, it's completely fine. Thanks for the heads up! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wzieba I left one comment, but other than that I think this is OK to merge as is. I am still tiny bit concerned about having multiple ways to use the SDK, but since you confirmed that it's safe to mix and match between them, I can't think of any major practical issues with it.
parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt
Outdated
Show resolved
Hide resolved
Instead of internal class `ParselyTrackerInternal`
So they're easily visible for consumers
Description
This PR is addressing 2 review comments from a different PR: #99 (comment) #99 (comment)
I identify 2 improvement opportunities here:
sharedInstance()
getter and confusing usageThis PR should address both matters. I'll put details in comments.
How to test
No need for manual tests.