Skip to content

Commit 3832186

Browse files
Atharva8168claude
andcommitted
feat(push): decouple auto push tracking and token forwarding (MAGE-886)
Replace the nested `disable_automatic_token_forwarding` escape hatch with a fully independent opt-in flag `automatic_token_forwarding`. The two push flags are now orthogonal: - `automatic_push_tracking` gates only automatic open tracking (trampoline) - `automatic_token_forwarding` gates only automatic token fetch/forwarding Both flags are unshipped (feature branch only), so there is no backward-compat cost. `KlaviyoPushService.onNewToken()` still forwards unconditionally; hosts opt out by registering their own FirebaseMessagingService. Also retargets the SdkFeatures telemetry entry to the new flag (dropping the inverted reportedValue); the `auto_push_token_forwarding` wire name is unchanged so the backend contract is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 444c50c commit 3832186

13 files changed

Lines changed: 109 additions & 77 deletions

File tree

sample/README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ demonstrated side by side:
2323
- **`manual`** — Manual integration (Option B). The app fetches the FCM token and calls `Klaviyo.setPushToken()`,
2424
and calls `Klaviyo.handlePush(intent)` on notification taps. This is the classic path and matches the
2525
behavior of prior sample releases.
26-
- **`automatic`** — Automatic integration (Option A). The app opts in with a single manifest flag
27-
(`com.klaviyo.push.automatic_push_tracking="true"`, see [src/automatic/AndroidManifest.xml](./src/automatic/AndroidManifest.xml))
28-
and the SDK does both for you: it auto-registers the push token at `initialize()` / every foreground, and
29-
routes notification taps through `KlaviyoTrampolineActivity` to track opens — so the sample's `SampleActivity`
30-
contains **zero** push boilerplate. Compare the two `SampleActivity.kt` copies to see exactly what code
31-
disappears when you opt in.
26+
- **`automatic`** — Automatic integration (Option A). The app opts in with two independent manifest flags
27+
(`com.klaviyo.push.automatic_push_tracking="true"` and `com.klaviyo.push.automatic_token_forwarding="true"`,
28+
see [src/automatic/AndroidManifest.xml](./src/automatic/AndroidManifest.xml)) and the SDK does both for you:
29+
`automatic_token_forwarding` auto-registers the push token at `initialize()` / every foreground, and
30+
`automatic_push_tracking` routes notification taps through `KlaviyoTrampolineActivity` to track opens — so
31+
the sample's `SampleActivity` contains **zero** push boilerplate. Compare the two `SampleActivity.kt` copies
32+
to see exactly what code disappears when you opt in.
3233

3334
Everything except `SampleActivity.kt` is shared under `src/main`. To switch styles, pick the **Build Variants**
3435
panel in Android Studio (`manualDebug` vs `automaticDebug`), or from the CLI:
@@ -40,8 +41,15 @@ panel in Android Studio (`manualDebug` vs `automaticDebug`), or from the CLI:
4041

4142
Both flavors share the same `applicationId` and `google-services.json`, so only one installs at a time. The
4243
`automatic` flavor still relies on the auto-registered `KlaviyoPushService` (from `:sdk:push-fcm`) to *display*
43-
notifications. To keep automatic open tracking while owning your own token pipeline, add
44-
`com.klaviyo.push.disable_automatic_token_forwarding="true"` to the manifest.
44+
notifications. Because the two flags are independent, you can mix and match: set only
45+
`automatic_push_tracking="true"` to keep automatic open tracking while owning your own token pipeline (omit
46+
`automatic_token_forwarding`), or set only `automatic_token_forwarding="true"` to auto-forward tokens without
47+
automatic open tracking.
48+
49+
Note that `KlaviyoPushService.onNewToken()` forwards token rotations to Klaviyo unconditionally whenever
50+
`KlaviyoPushService` is the registered `FirebaseMessagingService` — independent of both flags. To fully own
51+
the token pipeline, register your own `FirebaseMessagingService` instead of `KlaviyoPushService` (the same
52+
pattern used to integrate alongside Braze/Airship/Iterable).
4553

4654
See the main [README](../README.md) "Push Notifications" section for the full Option A / Option B write-up.
4755

sample/src/automatic/AndroidManifest.xml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,25 @@
22

33
<!--
44
SETUP NOTE (Automatic / Option A): This overlay is the ONLY difference in the automatic flavor's
5-
manifest. Setting com.klaviyo.push.automatic_push_tracking to "true" opts the app into:
6-
- automatic push OPEN tracking: notification taps route through KlaviyoTrampolineActivity, which
7-
calls Klaviyo.handlePush() for you (no handlePush() call in your Activity), and
8-
- automatic push TOKEN registration: the SDK fetches and registers the token at Klaviyo.initialize()
9-
and on every foreground (no FirebaseMessaging fetch / Klaviyo.setPushToken() in your app).
10-
Note the key's "com.klaviyo.push." prefix is required — the bare "com.klaviyo.*" form silently no-ops.
11-
The `manual` flavor omits this overlay entirely; absence of the flag is the manual (Option B) path.
12-
13-
Advanced: to keep automatic open tracking but own your token pipeline, also add
14-
com.klaviyo.push.disable_automatic_token_forwarding="true" (see the main README).
5+
manifest. It opts into both automatic behaviors via two independent flags:
6+
- com.klaviyo.push.automatic_push_tracking="true" — automatic push OPEN tracking: notification
7+
taps route through KlaviyoTrampolineActivity, which calls Klaviyo.handlePush() for you
8+
(no handlePush() call in your Activity), and
9+
- com.klaviyo.push.automatic_token_forwarding="true" — automatic push TOKEN registration: the SDK
10+
fetches and registers the token at Klaviyo.initialize() and on every foreground (no
11+
FirebaseMessaging fetch / Klaviyo.setPushToken() in your app).
12+
The two flags are fully independent — set either one without the other to opt into just that
13+
behavior. The `manual` flavor omits this overlay entirely; absence of both flags is the manual
14+
(Option B) path.
15+
Note the keys' "com.klaviyo.push." prefix is required — the bare "com.klaviyo.*" form silently no-ops.
1516
-->
1617
<application>
1718
<meta-data
1819
android:name="com.klaviyo.push.automatic_push_tracking"
1920
android:value="true" />
21+
<meta-data
22+
android:name="com.klaviyo.push.automatic_token_forwarding"
23+
android:value="true" />
2024
</application>
2125

2226
</manifest>

sample/src/automatic/java/com/klaviyo/sample/SampleActivity.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import com.klaviyo.analytics.model.EventMetric
1616

1717
/**
1818
* SETUP NOTE: This is the `automatic` flavor's Activity, demonstrating automatic push integration (Option A).
19-
* By setting com.klaviyo.push.automatic_push_tracking="true" in the manifest (see src/automatic/AndroidManifest.xml),
20-
* the SDK takes over both push responsibilities, so there is *zero* push boilerplate here:
21-
* - Push token: auto-registered at Klaviyo.initialize() and on every foreground (no FirebaseMessaging fetch,
22-
* no Klaviyo.setPushToken() call). Contrast with the `manual` flavor's onCreate under src/manual.
23-
* - Push opens: KlaviyoTrampolineActivity intercepts taps and calls Klaviyo.handlePush() for you, so there is
24-
* no handlePush() call in onNewIntent. Contrast with the `manual` flavor's onNewIntent.
19+
* By setting com.klaviyo.push.automatic_token_forwarding="true" and com.klaviyo.push.automatic_push_tracking="true"
20+
* in the manifest (see src/automatic/AndroidManifest.xml), the SDK takes over both push responsibilities, so
21+
* there is *zero* push boilerplate here:
22+
* - Push token (automatic_token_forwarding): auto-registered at Klaviyo.initialize() and on every foreground
23+
* (no FirebaseMessaging fetch, no Klaviyo.setPushToken() call). Contrast with the `manual` flavor's onCreate.
24+
* - Push opens (automatic_push_tracking): KlaviyoTrampolineActivity intercepts taps and calls Klaviyo.handlePush()
25+
* for you, so there is no handlePush() call in onNewIntent. Contrast with the `manual` flavor's onNewIntent.
2526
* Displaying notifications still relies on the auto-registered KlaviyoPushService from :sdk:push-fcm.
2627
* See the main README's "Push Notifications" section (Option A) and sample/README.md.
2728
*/

sample/src/manual/java/com/klaviyo/sample/SampleActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class SampleActivity : ComponentActivity() {
3535

3636
// SETUP NOTE (Manual / Option B): Fetch the current push token and register it with Klaviyo.
3737
// The `automatic` flavor omits this entirely — the SDK auto-registers the token at initialize()
38-
// and on every foreground once com.klaviyo.push.automatic_push_tracking is enabled.
38+
// and on every foreground once com.klaviyo.push.automatic_token_forwarding is enabled.
3939
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
4040
// Dispatch to main for the UI update
4141
lifecycleScope.launch(Dispatchers.Main) {

sdk/analytics/src/main/java/com/klaviyo/analytics/Klaviyo.kt

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -118,25 +118,21 @@ object Klaviyo {
118118
}
119119

120120
/**
121-
* Opt-in automatic push token registration (via [Constants.AUTOMATIC_PUSH_TRACKING]): pull the
121+
* Opt-in automatic push token registration (via [Constants.AUTOMATIC_TOKEN_FORWARDING]): pull the
122122
* current token and forward it to Klaviyo. Called from [initialize] and on each app foreground so
123-
* rotations are picked up. No-op when the flag is off, when forwarding is opted out via
124-
* [Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING], or when `push-fcm` is absent.
123+
* rotations are picked up. No-op when the flag is off or when `push-fcm` is absent.
124+
*
125+
* Independent of [Constants.AUTOMATIC_PUSH_TRACKING] (which gates only automatic open tracking) —
126+
* this flag alone controls token forwarding.
125127
*/
126128
internal fun maybeAutoRegisterPushToken() {
127-
val autoTrackingEnabled = Registry.config.getManifestBoolean(
128-
Constants.AUTOMATIC_PUSH_TRACKING,
129-
false
130-
)
131-
// Avoid a second manifest read on the common flag-off path
132-
val tokenForwardingDisabled = autoTrackingEnabled && Registry.config.getManifestBoolean(
133-
Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING,
129+
val tokenForwardingEnabled = Registry.config.getManifestBoolean(
130+
Constants.AUTOMATIC_TOKEN_FORWARDING,
134131
false
135132
)
136-
if (!autoTrackingEnabled || tokenForwardingDisabled) {
133+
if (!tokenForwardingEnabled) {
137134
Registry.log.verbose(
138-
"Skipping automatic push token registration " +
139-
"(automaticPushTracking=$autoTrackingEnabled, tokenForwardingDisabled=$tokenForwardingDisabled)"
135+
"Skipping automatic push token registration (automaticTokenForwarding=false)"
140136
)
141137
return
142138
}

sdk/analytics/src/test/java/com/klaviyo/analytics/KlaviyoTest.kt

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -596,38 +596,51 @@ internal class KlaviyoTest : BaseTest() {
596596
mockConfig.getManifestBoolean(Constants.AUTOMATIC_PUSH_TRACKING, false)
597597
} returns enabled
598598

599-
private fun setTokenForwardingDisabled(disabled: Boolean) = every {
600-
mockConfig.getManifestBoolean(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING, false)
601-
} returns disabled
599+
private fun setTokenForwardingEnabled(enabled: Boolean) = every {
600+
mockConfig.getManifestBoolean(Constants.AUTOMATIC_TOKEN_FORWARDING, false)
601+
} returns enabled
602602

603603
private fun reinitialize() =
604604
Klaviyo.initialize(apiKey = API_KEY, applicationContext = mockContext)
605605

606606
@Test
607-
fun `initialize triggers automatic push token fetch when flag is on and fetcher is registered`() {
607+
fun `initialize triggers automatic push token fetch when token forwarding is on and fetcher is registered`() {
608608
val mockFetcher = registerMockPushTokenFetcher()
609-
setAutomaticPushTracking(true)
609+
setTokenForwardingEnabled(true)
610610

611611
reinitialize()
612612

613613
verify(exactly = 1) { mockFetcher.fetchAndSetPushToken() }
614614
}
615615

616616
@Test
617-
fun `initialize does not fetch push token when token forwarding is disabled`() {
617+
fun `initialize fetches push token when token forwarding is on even if automatic push tracking is off`() {
618+
// Independence: token forwarding no longer depends on the open-tracking flag
618619
val mockFetcher = registerMockPushTokenFetcher()
619-
setAutomaticPushTracking(true)
620-
setTokenForwardingDisabled(true)
620+
setAutomaticPushTracking(false)
621+
setTokenForwardingEnabled(true)
622+
623+
reinitialize()
624+
625+
verify(exactly = 1) { mockFetcher.fetchAndSetPushToken() }
626+
}
627+
628+
@Test
629+
fun `initialize does not fetch push token when token forwarding is off`() {
630+
val mockFetcher = registerMockPushTokenFetcher()
631+
setTokenForwardingEnabled(false)
621632

622633
reinitialize()
623634

624635
verify(inverse = true) { mockFetcher.fetchAndSetPushToken() }
625636
}
626637

627638
@Test
628-
fun `initialize does not fetch push token when automatic tracking flag is off`() {
639+
fun `initialize does not fetch push token when only automatic push tracking is on`() {
640+
// Independence: automatic push tracking alone must not trigger token forwarding
629641
val mockFetcher = registerMockPushTokenFetcher()
630-
setAutomaticPushTracking(false)
642+
setAutomaticPushTracking(true)
643+
setTokenForwardingEnabled(false)
631644

632645
reinitialize()
633646

@@ -637,7 +650,7 @@ internal class KlaviyoTest : BaseTest() {
637650
@Test
638651
fun `initialize does not crash and logs a warning when the push token fetch throws`() {
639652
val mockFetcher = registerMockPushTokenFetcher()
640-
setAutomaticPushTracking(true)
653+
setTokenForwardingEnabled(true)
641654
every { mockFetcher.fetchAndSetPushToken() } throws RuntimeException("fetch blew up")
642655

643656
// runCatching around the fetch must contain the failure so initialize still completes
@@ -650,7 +663,7 @@ internal class KlaviyoTest : BaseTest() {
650663
@Test
651664
fun `initialize does not crash when flag is on but no push token fetcher is registered`() {
652665
Registry.unregister<PushTokenFetcher>()
653-
setAutomaticPushTracking(true)
666+
setTokenForwardingEnabled(true)
654667

655668
// push-fcm absent: lookup is null and automatic registration is a graceful no-op
656669
reinitialize()

sdk/analytics/src/test/java/com/klaviyo/analytics/networking/requests/EventApiRequestTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ internal class EventApiRequestTest : BaseApiRequestTest<EventApiRequest>() {
239239
@Test
240240
fun `Does not include the SDK features header even when the flags that would trigger it on PushTokenApiRequest are set`() {
241241
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_PUSH_TRACKING) } returns true
242-
every { mockConfig.hasManifestKey(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING) } returns true
242+
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_TOKEN_FORWARDING) } returns true
243243
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_PUSH_TRACKING, false) } returns true
244-
every { mockConfig.getManifestBoolean(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING, false) } returns true
244+
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_TOKEN_FORWARDING, false) } returns true
245245
val request = EventApiRequest(stubEvent, stubProfile)
246246
assertNull(request.headers["X-Klaviyo-Sdk-Features"])
247247
}

sdk/analytics/src/test/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequestTest.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,17 +113,18 @@ internal class PushTokenApiRequestTest : BaseApiRequestTest<PushTokenApiRequest>
113113

114114
@Test
115115
fun `SDK features header includes only auto_push_token_forwarding when only that key is present`() {
116-
every { mockConfig.hasManifestKey(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING) } returns true
117-
every { mockConfig.getManifestBoolean(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING, false) } returns true
116+
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_TOKEN_FORWARDING) } returns true
117+
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_TOKEN_FORWARDING, false) } returns true
118118
val request = PushTokenApiRequest(PUSH_TOKEN, stubProfile)
119-
assertEquals("auto_push_token_forwarding=0;", request.headers["X-Klaviyo-Sdk-Features"])
119+
assertEquals("auto_push_token_forwarding=1;", request.headers["X-Klaviyo-Sdk-Features"])
120120
}
121121

122122
@Test
123-
fun `SDK features header includes both attributes when both keys present and forwarding left at its default`() {
123+
fun `SDK features header includes both attributes when both keys present and forwarding on`() {
124124
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_PUSH_TRACKING) } returns true
125-
every { mockConfig.hasManifestKey(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING) } returns true
125+
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_TOKEN_FORWARDING) } returns true
126126
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_PUSH_TRACKING, false) } returns true
127+
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_TOKEN_FORWARDING, false) } returns true
127128
val request = PushTokenApiRequest(PUSH_TOKEN, stubProfile)
128129
assertEquals(
129130
"auto_push_tracking=1; auto_push_token_forwarding=1;",
@@ -132,11 +133,11 @@ internal class PushTokenApiRequestTest : BaseApiRequestTest<PushTokenApiRequest>
132133
}
133134

134135
@Test
135-
fun `SDK features header includes both attributes when both keys present and forwarding explicitly disabled`() {
136+
fun `SDK features header includes both attributes when both keys present and forwarding off`() {
136137
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_PUSH_TRACKING) } returns true
137-
every { mockConfig.hasManifestKey(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING) } returns true
138+
every { mockConfig.hasManifestKey(Constants.AUTOMATIC_TOKEN_FORWARDING) } returns true
138139
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_PUSH_TRACKING, false) } returns true
139-
every { mockConfig.getManifestBoolean(Constants.DISABLE_AUTOMATIC_TOKEN_FORWARDING, false) } returns true
140+
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_TOKEN_FORWARDING, false) } returns false
140141
val request = PushTokenApiRequest(PUSH_TOKEN, stubProfile)
141142
assertEquals(
142143
"auto_push_tracking=1; auto_push_token_forwarding=0;",

sdk/analytics/src/test/java/com/klaviyo/analytics/state/StateSideEffectsTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ class StateSideEffectsTest : BaseTest() {
419419

420420
@Test
421421
fun `Resumed lifecycle event re-fetches push token when automatic forwarding is enabled`() {
422-
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_PUSH_TRACKING, false) } returns true
422+
every { mockConfig.getManifestBoolean(Constants.AUTOMATIC_TOKEN_FORWARDING, false) } returns true
423423
val mockFetcher = registerMockPushTokenFetcher()
424424

425425
fireResumedEvent()
@@ -429,7 +429,7 @@ class StateSideEffectsTest : BaseTest() {
429429

430430
@Test
431431
fun `Resumed lifecycle event does not re-fetch push token when automatic forwarding is disabled`() {
432-
// Automatic push tracking flag defaults to false via BaseTest's getManifestBoolean stub
432+
// Automatic token forwarding flag defaults to false via BaseTest's getManifestBoolean stub
433433
val mockFetcher = registerMockPushTokenFetcher()
434434

435435
fireResumedEvent()

sdk/core/src/main/java/com/klaviyo/core/Constants.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ object Constants {
5151
const val AUTOMATIC_PUSH_TRACKING = PUSH_PREFIX + "automatic_push_tracking"
5252

5353
/**
54-
* Manifest `<meta-data>` boolean to opt out of automatic push token forwarding while keeping
55-
* automatic open tracking. Only consulted when [AUTOMATIC_PUSH_TRACKING] is `true`; defaults to
56-
* `false`. For hosts that own their own push-token pipeline.
54+
* Manifest `<meta-data>` key a host app sets to opt into automatic push token forwarding: the
55+
* SDK pulls the current push token at initialize and on each foreground and forwards it to
56+
* Klaviyo. Opt-in, absent → `false`.
57+
*
58+
* Fully independent of [AUTOMATIC_PUSH_TRACKING] — either flag may be set without the other.
59+
* Lives in core (not push-fcm) for the same reason as [AUTOMATIC_PUSH_TRACKING]: telemetry's
60+
* push token request must read it, and core cannot depend on push-fcm.
5761
*/
58-
const val DISABLE_AUTOMATIC_TOKEN_FORWARDING = PUSH_PREFIX + "disable_automatic_token_forwarding"
62+
const val AUTOMATIC_TOKEN_FORWARDING = PUSH_PREFIX + "automatic_token_forwarding"
5963

6064
/**
6165
* Fixed notification ID used in all notify/cancel calls.

0 commit comments

Comments
 (0)