Skip to content

Commit 6750d5a

Browse files
committed
simplified
1 parent 30b5cf6 commit 6750d5a

File tree

3 files changed

+50
-76
lines changed

3 files changed

+50
-76
lines changed

location/src/main/kotlin/io/customer/location/LocationTracker.kt

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,24 @@ import io.customer.sdk.util.EventNames
99

1010
/**
1111
* Coordinates all location state management: persistence, restoration,
12-
* profile enrichment, and sending location track events.
12+
* identify context enrichment, and sending location track events.
1313
*
14-
* Implements [IdentifyContextProvider] to enrich identify event context
15-
* with the latest location. Sends location track events directly via
16-
* [DataPipeline], applying the userId gate and sync filter locally.
14+
* Location reaches the backend through two independent paths:
1715
*
18-
* Tracks the last known userId to detect profile switches and reset
19-
* the sync filter accordingly.
16+
* 1. **Identify context enrichment** — implements [IdentifyContextProvider].
17+
* Every identify() call enriches the event context with the latest
18+
* location coordinates. This is unfiltered — a new user always gets
19+
* the device's current location on their profile immediately.
20+
*
21+
* 2. **"Location Update" track event** — sent via [DataPipeline.track].
22+
* Gated by a userId check and a sync filter (24h / 1km threshold)
23+
* to avoid redundant events. This creates a discrete event in the
24+
* user's activity timeline for journey/segment triggers.
25+
*
26+
* Profile switch handling is intentionally not tracked here.
27+
* On clearIdentify(), [onReset] clears all state (cache, persistence,
28+
* sync filter). On identify(), the new user's profile receives the
29+
* location via path 1 regardless of the sync filter's state.
2030
*/
2131
internal class LocationTracker(
2232
private val dataPipeline: DataPipeline?,
@@ -28,9 +38,6 @@ internal class LocationTracker(
2838
@Volatile
2939
private var lastLocation: LocationCoordinates? = null
3040

31-
@Volatile
32-
private var lastKnownUserId: String? = null
33-
3441
override fun getIdentifyContext(): Map<String, Any> {
3542
val location = lastLocation ?: return emptyMap()
3643
return mapOf(
@@ -65,41 +72,35 @@ internal class LocationTracker(
6572
}
6673

6774
/**
68-
* Called when the user identity changes (identify or clearIdentify).
69-
* Detects profile switches to reset the sync filter, then attempts
70-
* to sync the cached location for the new user.
75+
* Called when a user is identified. Attempts to sync the cached
76+
* location as a track event for the newly identified user.
77+
*
78+
* The identify event itself already carries location via
79+
* [getIdentifyContext] — this method handles the supplementary
80+
* "Location Update" track event, subject to the sync filter.
7181
*/
72-
fun onUserChanged(userId: String?, anonymousId: String) {
73-
val previousUserId = lastKnownUserId
74-
lastKnownUserId = userId
75-
76-
// Detect profile switch: previous user existed and differs from new user
77-
if (previousUserId != null && previousUserId != userId) {
78-
logger.debug("Profile switch detected ($previousUserId -> $userId), clearing sync filter")
79-
locationSyncFilter.clearSyncedData()
80-
}
81-
82-
if (!userId.isNullOrEmpty()) {
83-
syncCachedLocationIfNeeded()
84-
}
82+
fun onUserIdentified() {
83+
syncCachedLocationIfNeeded()
8584
}
8685

8786
/**
8887
* Clears all location state on identity reset (clearIdentify).
89-
* Resets in-memory cache, persisted location, and sync filter.
88+
* Resets in-memory cache, persisted location, and sync filter —
89+
* similar to how device tokens and other per-user state are
90+
* cleared on reset.
9091
*/
9192
fun onReset() {
9293
lastLocation = null
93-
lastKnownUserId = null
9494
locationPreferenceStore.clearCachedLocation()
9595
locationSyncFilter.clearSyncedData()
9696
logger.debug("Location state reset")
9797
}
9898

9999
/**
100100
* Re-evaluates the cached location for sending.
101-
* Called on identify (via [onUserChanged]) and on cold start to handle
102-
* cases where a location was cached but not yet sent for the current user.
101+
* Called on identify (via [onUserIdentified]) and on cold start
102+
* (via replayed UserChangedEvent) to handle cases where a location
103+
* was cached but not yet sent for the current user.
103104
*/
104105
internal fun syncCachedLocationIfNeeded() {
105106
val lat = locationPreferenceStore.getCachedLatitude() ?: return

location/src/main/kotlin/io/customer/location/ModuleLocation.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,22 @@ class ModuleLocation @JvmOverloads constructor(
7777

7878
locationTracker.restorePersistedLocation()
7979

80-
// Register as IdentifyContextProvider so location is added to identify event context
80+
// Register as IdentifyContextProvider so location is added to identify event context.
81+
// This ensures every identify() call carries the device's current location
82+
// in the event context — the primary way location reaches a user's profile.
8183
SDKComponent.identifyContextRegistry.register(locationTracker)
8284

8385
eventBus.subscribe<Event.ResetEvent> {
8486
locationTracker.onReset()
8587
}
8688

89+
// On identify, attempt to send a supplementary "Location Update" track event.
90+
// The identify event itself already carries location via context enrichment —
91+
// this track event is for journey/segment triggers in the user's timeline.
8792
eventBus.subscribe<Event.UserChangedEvent> {
88-
locationTracker.onUserChanged(it.userId, it.anonymousId)
93+
if (!it.userId.isNullOrEmpty()) {
94+
locationTracker.onUserIdentified()
95+
}
8996
}
9097

9198
val locationProvider = FusedLocationProvider(context)

location/src/test/java/io/customer/location/LocationTrackerTest.kt

Lines changed: 11 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class LocationTrackerTest {
5252
}
5353

5454
@Test
55-
fun givenLocationReceived_noUserIdentified_expectTrackNotCalled() {
55+
fun givenLocationReceived_noUserId_expectTrackNotCalled() {
5656
every { dataPipeline.isUserIdentified } returns false
5757

5858
tracker.onLocationReceived(37.7749, -122.4194)
@@ -167,47 +167,14 @@ class LocationTrackerTest {
167167
context["location_longitude"] shouldBeEqualTo -122.4194
168168
}
169169

170-
// -- onUserChanged --
170+
// -- onUserIdentified --
171171

172172
@Test
173-
fun givenProfileSwitch_expectClearsSyncFilter() {
174-
every { store.getCachedLatitude() } returns null
175-
176-
// First call sets lastKnownUserId
177-
tracker.onUserChanged("user-a", "anon-1")
178-
179-
// Switch to different user
180-
tracker.onUserChanged("user-b", "anon-1")
181-
182-
verify { syncFilter.clearSyncedData() }
183-
}
184-
185-
@Test
186-
fun givenFirstIdentify_expectNoClearSyncFilter() {
187-
every { store.getCachedLatitude() } returns null
188-
189-
tracker.onUserChanged("user-a", "anon-1")
190-
191-
// First identify should not clear (no previous user)
192-
verify(exactly = 0) { syncFilter.clearSyncedData() }
193-
}
194-
195-
@Test
196-
fun givenSameUserIdentify_expectNoClearSyncFilter() {
197-
every { store.getCachedLatitude() } returns null
198-
199-
tracker.onUserChanged("user-a", "anon-1")
200-
tracker.onUserChanged("user-a", "anon-1")
201-
202-
verify(exactly = 0) { syncFilter.clearSyncedData() }
203-
}
204-
205-
@Test
206-
fun givenUserChangedWithUserId_expectSyncsCachedLocation() {
173+
fun givenUserIdentified_withCachedLocation_expectSyncsCachedLocation() {
207174
every { store.getCachedLatitude() } returns 37.7749
208175
every { store.getCachedLongitude() } returns -122.4194
209176

210-
tracker.onUserChanged("user-a", "anon-1")
177+
tracker.onUserIdentified()
211178

212179
verify {
213180
dataPipeline.track(
@@ -218,32 +185,31 @@ class LocationTrackerTest {
218185
}
219186

220187
@Test
221-
fun givenUserChangedWithUserId_expectSyncFilterConsulted() {
188+
fun givenUserIdentified_withCachedLocation_expectSyncFilterConsulted() {
222189
every { store.getCachedLatitude() } returns 37.7749
223190
every { store.getCachedLongitude() } returns -122.4194
224191

225-
tracker.onUserChanged("user-a", "anon-1")
192+
tracker.onUserIdentified()
226193

227194
verify { syncFilter.filterAndRecord(37.7749, -122.4194) }
228195
}
229196

230197
@Test
231-
fun givenUserChangedWithUserId_filterRejects_expectNoTrack() {
198+
fun givenUserIdentified_filterRejects_expectNoTrack() {
232199
every { store.getCachedLatitude() } returns 37.7749
233200
every { store.getCachedLongitude() } returns -122.4194
234201
every { syncFilter.filterAndRecord(any(), any()) } returns false
235202

236-
tracker.onUserChanged("user-a", "anon-1")
203+
tracker.onUserIdentified()
237204

238205
verify(exactly = 0) { dataPipeline.track(any(), any()) }
239206
}
240207

241208
@Test
242-
fun givenUserChangedWithNullUserId_expectNoSync() {
243-
every { store.getCachedLatitude() } returns 37.7749
244-
every { store.getCachedLongitude() } returns -122.4194
209+
fun givenUserIdentified_noCachedLocation_expectNoTrack() {
210+
every { store.getCachedLatitude() } returns null
245211

246-
tracker.onUserChanged(null, "anon-1")
212+
tracker.onUserIdentified()
247213

248214
verify(exactly = 0) { dataPipeline.track(any(), any()) }
249215
}

0 commit comments

Comments
 (0)