Audience: Vodafone Turkey Android developers Goal: Move the Adobe Target API call + JSON handling out of the Tealium iQ WebView and into native Kotlin, so the at.js JavaScript tag can be switched off. Tealium iQ stays your rules engine — a lightweight Remote Command tag decides when to fire Target using the load rules you already maintain. The WebView remains for your other tags; it simply stops doing any Adobe Target work.
This is a deliberate, low-risk first step. It preserves your iQ agility today and is a natural stepping stone to the longer-term Prism / server-side move.
Before
trackView/Event ─► iQ WebView ─► at.js loads ─► Config.js + getOffer extensions decide + call
Adobe Target (getOffer over XHR in the WebView) ─► post-process
in JS ─► hand offer JSON back to app via Remote Command
After
trackView/Event ─► iQ WebView ─► "Adobe Target" Remote Command TAG fires (gated by your iQ load
rules) ─► emits tealium://adobe_target?request={…}
│
▼ (native, no JS)
AdobeTargetRemoteCommand.onInvoke ─► AdobeTargetClient calls Adobe Target Delivery API
─► parses offer JSON ─► onAdobeTargetOffers(...) renders in the app
at.js Adobe Target tag = OFF
The WebView still initialises (for your other tags), but it no longer loads at.js, no longer runs the Target mbox/getOffer logic, and the offer JSON no longer crosses the JS↔native bridge.
Turn OFF the Adobe Target at.js tag and its Target-specific extensions
(AT.js Import, Configuration, On Request Success Window Methods, GetOffer Trigger,
Profile Reset). These only exist to run Target inside the WebView.
Add / keep a "Tealium Remote Command" tag in the same profile:
adobe_target (must match the native command — see §3)Config.js and the getOffer trigger. This is where your agility stays:
you add/remove pages and tweak conditions in iQ without an app release. Your 5% control group
can stay here too (a load rule on a random-bucketing variable).Because the trigger and its rules live on this tag, the WebView only needs to load utag.js + this tag, not at.js.
Note: A load rule is a condition on data-layer variables (equals / contains / regex, AND/OR). Most
Config.jspage rules map directly. If a decision needs logic a load rule can't express, a tiny iQ extension that sets a boolean flag is fine — that is lightweight JS, not at.js.
Four files (already drafted):
| File | Role |
|---|---|
AdobeTargetClient.kt |
Native Adobe Target Delivery API v1 client (build request, HTTP, parse offers, persist session/tntId/edgeHost). |
AdobeTargetRemoteCommand.kt |
Subclasses the SDK RemoteCommand (id adobe_target); reads the payload, acks the WebView immediately, runs the call off-thread. |
AdobeTargetPolicy.kt |
Native replacement for the at.js view/session-limit + delay + per-component priority logic (see §5). Optional — wire it into your render hook. |
TealiumHelper.kt (edited) |
Constructs the client and registers the command alongside your existing ones. |
Registration — already added to TealiumHelper.buildRemoteCommands(); it's a plain
remoteCommands?.add(AdobeTargetRemoteCommand(...)) (the WebView-triggered path — not the JSON
filename/remoteUrl variant, so you keep iQ's rule agility).
Two config values you must supply (via BuildConfig):
ADOBE_TARGET_CLIENT_CODE — your Adobe Target client code / tenant (the xxxx in
xxxx.tt.omtrdc.net). New value to add.config.adobeVisitorOrgId
(BuildConfig.ADOBE_ORG_ID). No new value needed.Render the offer — set the callback once (e.g. in TealiumHelper.init or app startup) to route
offers into your existing state objects, exactly as the WebView Remote Command used to:
TealiumHelper.data-removed= { result ->
result.offers.forEach { offer ->
offer.options.forEach { option ->
// option.type = html | json | redirect | dynamic | actions
// option.content = String (html/redirect) or JSONObject/JSONArray (json/actions)
when (offer.mbox) {
"target-global-mbox" -> HomePageResponseState.setAddonTargetResponse(/* map option.content */)
// …route other mboxes to the relevant *ResponseState as today
}
}
}
}
Identity (ECID → marketingCloudVisitorId, AAM region/blob) is sourced automatically from the
AdobeVisitor collector you already configure in TealiumInitializer — no extra work.
Map your data-layer variables to these keys; all optional except as noted. Anything Target-specific
you send as mbox_parameters / profile_parameters is forwarded verbatim.
| Payload key | Type | Meaning |
|---|---|---|
mbox |
String | Regional mbox name. Defaults to target-global-mbox. |
mbox_index |
Int | Defaults to 0. |
page_url |
String | Current page/screen → context.address.url. |
mbox_parameters |
Object | name/value mbox parameters. |
profile_parameters |
Object | name/value profile parameters. |
product |
Object | { "id": "...", "categoryId": "..." }. |
order |
Object | { "id": "...", "total": 0.0, "purchasedProductIds": ["..."] }. |
Adobe constraint: mbox parameter names must not be
orderId/orderTotal/productPurchasedIds(put those inorder) and must not start withprofile.. Max 50 params.
Turning off at.js also removes the JS that ran on the Target response (steps 3, 7, 8 in your architecture deck). These operate on the response, which is now native.
AdobeTargetPolicy.ktThis is the main net-new native work, and it's drafted for you in AdobeTargetPolicy.kt. It is the
direct equivalent of the at.js "On Request Success Window Methods" (View Limit, Session View Limit,
Delay Time) plus the per-component priority selection, and the "Profile Reset". It:
State persists in SharedPreferences, mirroring the old JS containers
(tealiumAdobeTargetHistory / …ViewLimitContainer / …SessionLimitContainer); per-session counts
live in a session-scoped namespace cleared after 30 min of inactivity.
Where the limit/priority values come from: Adobe Target response tokens on each option
(activity id, priority, view limit, session limit, delay seconds, and optionally component).
Enable them in the Target UI (Administration ▸ Response Tokens) or emit them from an
activity/profile script, then point AdobeTargetPolicy.Config at your token names. If a limit token
is absent, that limit is treated as unlimited (nothing is wrongly suppressed). Component/slot
resolution is pluggable via Config.slotResolver — set it to your component model so mutually
exclusive experiences contest the same slot.
Wire it into the render hook:
private val targetPolicy = AdobeTargetPolicy(
context = application,
config = AdobeTargetPolicy.Config(
priorityToken = "activity.priority", // ← your response-token names
viewLimitToken = "view.limit",
sessionLimitToken = "session.limit",
delaySecondsToken = "delay.seconds",
slotResolver = { _, option ->
option.responseTokens?.opt("component")?.toString() ?: "default"
},
),
)
TealiumHelper.data-removed= { result ->
targetPolicy.select(result).forEach { decision ->
val shown = render(decision.slot, decision.candidate.option) // your UI code; return true if displayed
if (shown) targetPolicy.recordShown(decision) // only then does it count
}
}
Call targetPolicy.reset() where the old flow did a Profile Reset (e.g. logout).
The client already parses analytics.payload (pe/tnta) into result.analyticsPayloads. Choose
one owner:
analyticsLogging = "server_side" when constructing
AdobeTargetClient and Adobe logs the A4T hit on the edge; orpe/tnta to Adobe Analytics from onAdobeTargetOffers.Do not leave both the old at.js beacon and a native one firing.
The drafted client reuses Adobe's returned tntId and edgeHost, keeps a stable 30-minute
sessionId, and resets that identity if the ECID changes (login/logout). No action needed.
The point is not that at.js blocks on the network wait — getOffer is an async XHR, so the JS
event loop is free during the round-trip. The gains come from removing everything around the
request from the WebView's single, shared JS thread:
JSON.parse of a page-level
payload (all eligible activities) plus the post-processing extensions (filtering, limits,
formatting) — runs as one uninterruptible task that also queues behind every other tag's JS on
that one thread. On a low-RAM/slow-CPU device, that serialization is where the jank lives.HttpURLConnection.Our native command acks the WebView instantly (response.send() fires the moment the trigger is
received) and does the request setup, HTTP call, JSON parse and post-processing on a background
thread (Dispatchers.IO), with connection reuse to the omtrdc edge (it pins edgeHost). So the
WebView's JS thread is released immediately and never touches the network, the response, or the
bridge — strictly less WebView work than the at.js path, even accounting for the async point above.
What this does not do: make the network round-trip itself faster (that's edge/server-bound). If a UI render is gated on the offer, that latency remains.
Be clear-eyed: the WebView stays (you have other WebView-dependent tags), so this is a targeted reduction, not the full memory win. What you remove is real and specific to the Target complaint: loading/parsing at.js, executing the Config.js + getOffer + post-processing extensions, and marshalling the offer JSON across the JS bridge on every eligible page.
Measure on your actual low-RAM target devices (2–3 GB), release build, throttled network — before vs after:
JankStats,
Choreographer dropped frames). This is where you should see the clearest gain.adb shell dumpsys meminfo <pkg>; watch the :sandboxed_process renderer). Expect
a smaller move here until the WebView itself can go — that's the Prism / server-side step.If the numbers show even a modest, consistent improvement on low-RAM devices, that's the goodwill win — and it sets up the Prism migration (Prism has no WebView at all) as the long-term fix.
ADOBE_TARGET_CLIENT_CODE to BuildConfig.TealiumHelper.onAdobeTargetOffers to your render state.adobe_target Remote Command tag with load rules + data mappings.component), and point AdobeTargetPolicy.Config at their names + set slotResolver.AdobeTargetPolicy into the render hook (select → render → recordShown); call
reset() on logout. (Or defer limits initially and accept simplified behaviour.)