Skip to content

Conversation

@nan-li
Copy link
Contributor

@nan-li nan-li commented Oct 10, 2025

Description

One Line Summary

Start detecting timezone changes and sending it up when it changes.

Details

Motivation

This was a request of clients so that when users enter new timezones, they can be properly segmented without needing a Create User call to happen.

Scope

  • No longer hydrate timezone from remote (this aligns with existing iOS behavior)
  • When timezone changes on the device, the new one will be sent
  • We don't need to address the migration and legacy player issue that had to be addressed by iOS: Detect for timezone changes and update the user OneSignal-iOS-SDK#1542. These PR changes automatically address them because missing timezone cases will get the timezone sent on the next session (since locally it is null which will trigger a change detection).

Testing

Unit testing

Added

Manual testing

Change timezone on emulator and see the next session, that timezone is sent with the track session start call.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

This change is Reviewable

Tack onto existing SessionListener's TrackSessionStartOperation call so this update would be combined. Otherwise, because the TrackSessionStartOperation has flush = true, the timing may mean 2 user update requests are made when it could be combined.

Cons: the Session Listener is encroaching  beyond what its scope should be.
@nan-li nan-li force-pushed the feat/update_timezone branch 2 times, most recently from dbb8bde to f5dc3c9 Compare October 10, 2025 00:29
After the implementation to start updating timezone changes on new sessions, hydrating timezone from the user refresh call will conflict if the timing between the update and refresh requests is not perfect. For example, imagine this scenario if we still hydrate this property:
1. On a new session, timezone detects as changed from US to UK. A property update is enqueued.
2. This request is sent and the refresh user call returns (which still contains timezone as US)
3. The SDK hydrates timezone as US
4. On the next session, timezone is detected as changed from US to UK.
@nan-li nan-li force-pushed the feat/update_timezone branch from f5dc3c9 to 6018f4d Compare October 10, 2025 00:30
@nan-li nan-li requested review from a team, brismithers and jkasten2 October 13, 2025 20:46
Copy link
Contributor

@abdulraqeeb33 abdulraqeeb33 left a comment

Choose a reason for hiding this comment

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

Must write unit tests

if (response.properties.timezoneId != null) {
propertiesModel.timezone = response.properties.timezoneId
}
// No longer hydrate timezone from remote, set locally
Copy link
Contributor

Choose a reason for hiding this comment

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

same with this.

}

fun update() {
timezone = TimeUtils.getTimeZoneId()
Copy link
Contributor

Choose a reason for hiding this comment

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

does this TimeUtils.getTimeZoneId() match with what the backend really expects?
Can we write a test to make sure that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it does. Currently, the only place the SDK ever sends timezone is in a create user call here:

// the non-null assert is actually extraneous
properties["timezone_id"] = TimeUtils.getTimeZoneId()!!

does this TimeUtils.getTimeZoneId() match with what the backend really expects? Can we write a test to make sure that?

I don't know how we could write a test for what the server expects. We could write a test that ensures timezone is always sent as a certain format, but if the backend ever changed its expectation, we wouldn't know.

Copy link
Contributor

Choose a reason for hiding this comment

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

You are right, we cant test server expectations directly. But we should write tests to catch internal breaking changes.

Copy link
Member

Choose a reason for hiding this comment

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

I think the model classes should be very simple and all setting should done outside of it. So we shouldn't add a helper like this that is set a specific implementation of getting timezone.

propertiesModel.timezone = response.properties.timezoneId
}
// No longer hydrate timezone from remote, set locally
propertiesModel.update()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be done on a background thread?

Copy link
Contributor Author

@nan-li nan-li Oct 15, 2025

Choose a reason for hiding this comment

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

Same comment as before, propertiesModel.update() just sets propertiesModel.timezone = TimeUtils.getTimeZoneId(). If we look at the lines above this, we are setting various properties on the propertiesModel like:

propertiesModel.country = response.properties.country
propertiesModel.tags[tagKVP.key] = tagKVP.value!!
propertiesModel.language = response.properties.language

Setting timezone shouldn't run separately while this object is being set up. Also, during this time while the model is being set up, no updates will fire an operation to enqueue.

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good around the background thread. but we definitely need to write a test if the properties.update() is called and if the timezone is actually set.
this helps in possible regression defects and helps us gain confidence when we are making changes.

@nan-li
Copy link
Contributor Author

nan-li commented Oct 15, 2025

Must write unit tests

Yeah, I mentioned in PR that I will add if implementation is fine. I put the logic in the SessionListener, but for true separation of concerns, it should probably be in UserManager or something similar. However, putting in SessionListener can batch this operation with the track session start operation so we don't send 2 requests. But I'll make sure to write tests for the cases you pointed out.

}

fun update() {
timezone = TimeUtils.getTimeZoneId()
Copy link
Member

Choose a reason for hiding this comment

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

I think the model classes should be very simple and all setting should done outside of it. So we shouldn't add a helper like this that is set a specific implementation of getting timezone.

@abdulraqeeb33
Copy link
Contributor

#2378 (comment)

that makes sense to me as well @jkasten2 . Though I wonder if timezone would really changes between every app session. And i am guessing you would prefer to be more careful so better to do it onFocus?

Would it be more optimized if we update the timezone in the properties only if different from the current one. That way we dont update everytime on onFocus.

@jkasten2
Copy link
Member

jkasten2 commented Oct 16, 2025

#2378 (comment)

that makes sense to me as well @jkasten2 . Though I wonder if timezone would really changes between every app session. And i am guessing you would prefer to be more careful so better to do it onFocus?

I suggested using onFocus as this would result in picking up changes more often than a new session and decouples it from any session logic that could change in the future. Not concern about being careful of checking too often.

The best option would be if our SDK listens to timezone changes from the system (Android APIs) and can send that to our server. That way we always have the latest correct value without the user needing to open the app. Example code of how that can be done:
https://stackoverflow.com/questions/31398324/is-there-an-android-event-listener-for-change-in-the-timezone-of-the-device

However it's pretty complex to setup the Android APIs. Using something like onFocus gets us 90% of the benefits, is something that we can be happy with long term, and matches closer to iOS's behavior.

  • If we really want to update timezone as soon as it changes maybe we can see if iOS supports this as well.

Would it be more optimized if we update the timezone in the properties only if different from the current one. That way we dont update everytime on onFocus.

I believe our models already takes care of not doing anything if the value didn't change.

Let the UserManager refresh the changes to properties model, instead of SessionListener. The SubscriptionManager already refreshes the push subscription.
@nan-li nan-li force-pushed the feat/update_timezone branch from 2b8d221 to e433a4f Compare October 16, 2025 19:23
@nan-li
Copy link
Contributor Author

nan-li commented Oct 20, 2025

@jkasten2 @abdulraqeeb33 - I pushed a commit to have UserManager instead detect changes in onFocus.
By calling propertiesModel.timezone = TimeUtils.getTimeZoneId() within onFocus, this always happens before onSessionStarted so any timezone updates get enqueued before TrackSessionStart in SessionListener. The issue prior was that TrackSessionStart had flush=true so it does not wait 5 seconds on a new session for other updates.

AR started adding tests in #2382 🙏

Copy link
Contributor

@abdulraqeeb33 abdulraqeeb33 left a comment

Choose a reason for hiding this comment

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

thank you creating a new pr for tests


override fun onFocus(firedOnSubscribe: Boolean) {
// Detect any user properties updates that changed
_propertiesModel.timezone = TimeUtils.getTimeZoneId()
Copy link
Contributor

Choose a reason for hiding this comment

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

do you want to pass TimeUtils into this class? so that you can mock it?

Copy link
Contributor

Choose a reason for hiding this comment

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

also please make sure to write tests

Copy link
Contributor

@abdulraqeeb33 abdulraqeeb33 left a comment

Choose a reason for hiding this comment

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

As the tests are in the other PR

@nan-li
Copy link
Contributor Author

nan-li commented Oct 20, 2025

As the tests are in the other PR

Let's not merge this until then, we can pick those commits over when done

@nan-li
Copy link
Contributor Author

nan-li commented Oct 23, 2025

Hi @abdulraqeeb33 I used the changes you made in #2382 and modified to add the tests here. Let me know if a4a6d5e looks good to you.

* Add UserManagerTests that onFocus will update timezone
* Update an existing test in RefreshUserOperationExecutorTests that tests a successful getUser request and hydration already. Let's add on the timezone component to this hydration, which is that timezone is no longer hydrated from the server but set locally.
* Add TimeUtilsTest class testing getTimeZoneId
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants