Adobe Target — Native Remote Command Implementation Guide

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.


1. What changes

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.


2. iQ side (no app release needed for rule changes)

  1. 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.

  2. Add / keep a "Tealium Remote Command" tag in the same profile:

    • Command ID: adobe_target (must match the native command — see §3)
    • Load Rules: replicate your existing "should Target fire here?" logic — i.e. the page/event conditions currently in 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).
    • Data Mappings: map your data-layer variables to the payload keys the command reads (§4).

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.js page 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.


3. App side — files & wiring

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):

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.


4. Payload contract (iQ Remote Command data mappings)

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 in order) and must not start with profile.. Max 50 params.


5. What moved out of the WebView — and what you must decide

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.

5.1 View limit / session limit / delay + per-component priority — AdobeTargetPolicy.kt

This 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:

  1. turns each returned option into a candidate (identity, slot, priority, limits),
  2. drops candidates that hit their lifetime View Limit, their per-session limit, or are still inside their Delay window,
  3. selects the highest-priority survivor per component (slot),
  4. records an impression only when the app confirms the winner was shown — so limits count real impressions, exactly as the JS View Limit did.

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).

5.2 A4T (Analytics for Target) reporting

The client already parses analytics.payload (pe/tnta) into result.analyticsPayloads. Choose one owner:

Do not leave both the old at.js beacon and a native one firing.

5.3 Identity — already handled

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.


6. Expected improvement & what to measure

Why this helps (the precise mechanism)

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:

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.

What to measure

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:

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.


7. Rollout checklist