Skip to content
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

Migrate ParselyTracker from Java to Kotlin #99

Merged
merged 32 commits into from
Jan 17, 2024

Conversation

wzieba
Copy link
Collaborator

@wzieba wzieba commented Dec 13, 2023

Best review after #100

Description

This PR migrates ParselyTracker, from Java to Kotlin. This class is the starting point for any user of this SDK.

Aside from the migration, this PR introduces also default arguments for tracking methods, to make calling this SDK from Kotlin more convenient, some code simplifications. This PR also introduces enforcing visibility modifiers for the whole library by setting explicitApi to STRICT.

Testing

This PR does not require testing. Functional tests should assert the validity of basic flows.

To fix functional tests execution
The `sharedInstance` that creates a new instance of `ParselyTracker` should not have nullable return type
To improve API usage for both Kotlin and Java SDK consumers
Due to Java->Kotlin conversion, this condition was lost during merge.
…viewUuid`

I think there's no a lot of value in providing `null` in `extraData` items
Fix missing visibility modifiers
@wzieba wzieba changed the base branch from main to issue/migrate_events_builder_to_kotlin December 13, 2023 18:18
Copy link

codecov bot commented Dec 13, 2023

Codecov Report

Attention: 90 lines in your changes are missing coverage. Please review.

Comparison is base (2c64338) 71.81% compared to head (a8bf91d) 68.75%.
Report is 1 commits behind head on main.

❗ Current head a8bf91d differs from pull request most recent head c2cb617. Consider uploading reports for the commit c2cb617 to get more accurate results

Files Patch % Lines
.../java/com/parsely/parselyandroid/ParselyTracker.kt 24.36% 90 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #99      +/-   ##
==========================================
- Coverage   71.81%   68.75%   -3.07%     
==========================================
  Files          18       18              
  Lines         369      400      +31     
  Branches       52       49       -3     
==========================================
+ Hits          265      275      +10     
- Misses         92      113      +21     
  Partials       12       12              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@wzieba wzieba changed the title [WIP] Migrate ParselyTracker from Java to Kotlin Migrate ParselyTracker from Java to Kotlin Dec 13, 2023
@wzieba wzieba marked this pull request as ready for review December 13, 2023 19:23
* 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 open class ParselyTracker protected constructor(siteId: String, flushInterval: Int, c: Context) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be public and open but I haven't found any other way for now. If I introduced some interface, let's say Tracker to use it in EngagementManagerTest to create a fake implementation, I'd have to expose enqueueEvent to public - apparently, interface methods cannot be internal.

But I believe this shouldn't be a blocker - ParselyTracker, as previously written in Java, was "open" by default.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I introduced some interface, let's say Tracker to use it in EngagementManagerTest to create a fake implementation, I'd have to expose enqueueEvent to public - apparently, interface methods cannot be internal.

Assuming that this is what we wanted to do, wouldn't it be more appropriate to use multiple interfaces in this case? We'd have a Tracker interface that we expose to public and another internal interface that has enqueueEvent function. ParselyTracker then will implement both and everything should work? Let me know if I am missing anything in this scenario.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with multiple interfaces, we can't have a method in interface that is internal. Even if the interface itself is internal

image

If we didn't add internal, then ParselyTracker#enqueueEvent would be exposed to API consumers, which is something we should omit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very likely that I am missing something, but I was thinking something like this would work? 74a9f18

Note that this ^ is not production quality code, I just put it together to make it easier to discuss. Let me know what you think!


Also, if we did go with such a design, I think we can make ParselyTracker internal as well and expose the shared instance some other way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I really like your idea, haven't seen in this way myself. I also appreciate it clarifies the public contract better.

I've cherry-picked your comment and added some changes. Most notably, I've renamed Tracker interface to ParselyTracker to minimize breaking changes for the API consumers.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am glad you found it useful! 🥳

Copy link
Collaborator

@oguzkocer oguzkocer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wzieba I finished my first pass of this, but I might have more to add in my second pass as I want to think about it a bit. Hope you find the comments useful!

* 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 open class ParselyTracker protected constructor(siteId: String, flushInterval: Int, c: Context) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I introduced some interface, let's say Tracker to use it in EngagementManagerTest to create a fake implementation, I'd have to expose enqueueEvent to public - apparently, interface methods cannot be internal.

Assuming that this is what we wanted to do, wouldn't it be more appropriate to use multiple interfaces in this case? We'd have a Tracker interface that we expose to public and another internal interface that has enqueueEvent function. ParselyTracker then will implement both and everything should work? Let me know if I am missing anything in this scenario.

Base automatically changed from issue/migrate_events_builder_to_kotlin to main January 11, 2024 09:51
BREAKING CHANGE: This commit removes `setDebug` method which is a part of public contract. Instead, API consumers should set debug parameter when invoking `sharedInstance` method.
I believe it better communicates the intention than `debug` flag.
We either way were setting empty string in case of nullable `urlRef`. Now it's more explicit.

BREAKING CHANGE: As `urlRef` values are no longer nullable, this breaks the public contract.
@wzieba
Copy link
Collaborator Author

wzieba commented Jan 11, 2024

Thank you @oguzkocer for the review! Ready for the second round.

@wzieba
Copy link
Collaborator Author

wzieba commented Jan 15, 2024

Thank you @oguzkocer for the suggestions! Applied them, ready for another round.

Copy link
Collaborator

@oguzkocer oguzkocer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wzieba Thank you for addressing my previous feedback. I am glad that it was useful.

I had a one last look and wanted to bring up a couple points about the singleton. I am approving the changes just so this PR doesn't block you anymore, but I am curious to hear your thoughts on those.

In general, it's looking really good and the SDK is looking significantly better 🙇‍♂️

Comment on lines +65 to +73
/**
* Singleton instance accessor. Note: This must be called after [.sharedInstance]
*
* @return The singleton instance
*/
@JvmStatic
public fun sharedInstance(): ParselyTracker? {
return instance
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if the additional sharedInstance method is helpful or not. I think the following is the intended usage:

  • First call sharedInstance({configuration})
  • Subsequently call sharedInstance()

Although this works - the better approach in my opinion is to:

  • First call sharedInstance({configuration}) and assign it to a property (preferably somewhere where you can inject it)
  • Then only access the tracker through your local (to the client binary) property and not use the sharedInstance()

This approach is better in my opinion, because it doesn't deal with the nullability and somewhat forces the instantiation of the property to be done in a more proper place. It's also how a client would handle the tracker if it wasn't using a singleton pattern.

So, I wonder if we should not give them the option to use the first approach at all. What do you think?

Comment on lines +84 to +91
@JvmStatic
@JvmOverloads
public fun sharedInstance(
siteId: String,
flushInterval: Int = DEFAULT_FLUSH_INTERVAL_SECS,
context: Context,
dryRun: Boolean = false,
): ParselyTracker {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A singleton that requires a configuration presents an interesting problem. What happens if the client tries to pass a different configuration after the instance is already initialized?

Currently, the secondary configuration is silently ignored. And, I could see a case where this can result in developers trying to figure out why their configuration change is not applied. Assuming that we don't want to allow updating the configuration after the initialization, I think we can give them a heads up about this in the documentation and by printing a log.

How do you feel about:

  1. Adding a note to the documentation of this method, saying that this method should only be called once and subsequent calls will ignore its arguments and return the existing instance
  2. Either add a generic warning about this ^ as a log every time we return the existing instance. Or - add a specific warning if this method is called with a different configuration as compared to the existing instance.

Note that an alternative approach that would avoid this whole issue would be to use a build time configuration instead of function parameters, but I don't think that's appropriate for this use case. Even if it was, that approach has its own issues - but I wanted to mention it for completeness-sake.

@wzieba
Copy link
Collaborator Author

wzieba commented Jan 17, 2024

Thank you @oguzkocer for the comments about ParselyTracker#sharedInstance! I think it'll be the best to continue this discussion in a separate PR - merging this one, prepare changes in another PR and ping you there 🙂.

@wzieba wzieba merged commit d4e225d into main Jan 17, 2024
2 checks passed
@wzieba wzieba deleted the issue/parsely_tracker_kotlin_migration branch January 17, 2024 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants