如何將追蹤參數從行動網頁傳遞至商店安裝流程? 將追蹤參數從行動網頁傳遞至新安裝的 App,需要載入 OpoInstall Web JS SDK,擷取列入白名單的 URL 參數,並在 App 首次啟動時透過原生 SDK 還原已支援的歸因數據。
跨 App 商店下載的參數串接,是指當使用者從行動登陸頁面轉跳至 App 商店並安裝後,在啟動新安裝的 App 時,仍能保留符合條件之網頁 URL 參數的過程。Web SDK 會捕捉行銷活動或推薦內容,而平台支援的機制或延遲還原服務則能使相關參數可供原生 App 讀取。還原能力取決於平台、SDK 設定、可用的配對訊號以及適用的隱私權要求。
| 術語 | 定義 | 相關實體 | 搜尋意圖角色 |
|---|---|---|---|
| 追蹤參數 (Tracking Parameters) | 附加於 URL 後方的鍵值對,用於識別流量情境、行銷活動來源或推薦資訊。 | URL 查詢字串 | 資訊性 / 技術性 |
| App 參數安裝 (App Parameter Installation) | 一種延遲參數還原工作流程,可在安裝後取得符合資格的網頁情境數據。 | 原生行動 SDK | 技術性 / 資訊性 |
| Google Play 安裝參照 (Google Play Install Referrer) | Google Play 官方 API,用於檢索與 Google Play 安裝相關的參照元數據。 | Android SDK | 技術性 |
網頁至商店的歸因鴻溝:App 商店間的平台屏障

斷連屏障:作業系統沙盒如何隔離瀏覽器情境
網頁分析通常使用 URL 查詢字串,包括 UTM 標籤(utm_source、utm_medium、utm_campaign)、廣告點擊識別碼(如 gclid 和 fbclid)以及自訂的推薦參數。
在參與的網站內,瀏覽器 API 和第一方分析系統可為允許的分析與行銷工作流程擷取這些數值。
挑戰在於當訪客離開瀏覽器並進入 App 商店安裝流程時。瀏覽器與新安裝的原生應用程式在獨立的環境中執行。瀏覽器 Cookie、Session Storage 和頁面 URL 參數不會在安裝後自動傳輸至應用程式內。
因此,開發者需要商店支援的參照機制或延遲參數還原工作流程,將安裝前的有效情境與安裝後的 App 工作階段串接起來。
平台特性:Apple App Store 與 Google Play Store 的攝取差異

行動 App 商店支援不同的參照情境傳遞方式。
- Apple App Store (iOS): 標準的 App Store 安裝不會自動將外部的任意 URL 查詢參數轉發至新安裝應用程式的首次啟動環境。延遲還原服務或明確的使用者介入工作流程可能有助於復原符合資格的情境,且須符合 Apple 的隱私權要求。
- Google Play Store (Android): Google 提供了官方的 Google Play Install Referrer API。在採用正確架構的 Google Play 參照流程下,已安裝的應用程式可檢索支援的參照元數據,包括參照字串與相關時間戳記。
這些機制不應被視為可互換。Google Play 提供官方商店層級的參照介面,而 iOS 上的任意延遲參數還原則需要設計獨立且符合政策的機制。
未歸因安裝流量對行銷 ROI 計算的商業影響
若行銷活動參數在網頁瀏覽至 App 安裝之間遺失,分析系統可能無法將安裝正確歸因至原始活動。
這會導致出現未歸因安裝、歸因報告不完整,以及投資報酬率 (ROI) 或客戶獲取成本 (CAC) 計算不準確的問題。
參數遺失也會影響用戶 onboarding 體驗。例如,網頁推薦連結可能包含邀請者識別碼或優惠代碼:
https://example.com/download?inviter_id=USR_1024
https://example.com/download?promo_code=WELCOME50
如果應用程式無法恢復這些數值,使用者可能需要手動重新輸入。
然而,復原推薦或優惠數值並不代表自動獲獎。應用程式後端必須獨立驗證其資格、兌換上限及適用的商業規則。
商店支援與延遲參數機制:Apple App Store 與 Google Play 的比較
Google Play Store 機制:利用原生 Install Referrer API
在 Android 上,Google Play Install Referrer API 提供了檢索支援安裝參照元數據的官方機制。
Google Play 下載 URL 可包含一個編碼後的 referrer 參數:
https://play.google.com/store/apps/details?id=com.example.app&referrer=utm_source%3Dfacebook%26inviter_id%3DUSR_1024
在此範例 URL 中,編碼後的參照值代表:
utm_source=facebook&inviter_id=USR_1024
安裝後,原生 Android 應用程式可使用 Install Referrer Client 來檢索可從 Google Play 獲取的參照資訊。
特定版本的 OpoInstall Web SDK 是否會自動產生、修改或管理此類 URL,須根據其發布的文件與行為進行驗證。Google 官方 API 的存在並不能直接定義第三方 SDK 的內部實現。
Apple App Store 邊界:處理無原生查詢參數的標準商店轉跳
Apple App Store 不會直接將網頁查詢參數傳遞至新安裝應用程式的首次啟動回調中。
因此,延遲還原工作流程可能涉及在商店轉跳前捕捉允許的活動資訊,並在原生應用程式啟動時請求相關情境。
對於基於 SDK 的工作流程,開發者必須區分供應商記錄的功能與部署的 SDK 版本所使用的配對方法。
任何配對或數據收集方法皆須符合 Apple 的隱私政策。特別是,歸因實現不得依賴禁止的裝置指紋識別,也不應假定取得 ATT 許可即可使本不被允許的配對方法變得合規。
通用連結 (Universal Links) 對於將連結導向已安裝應用程式很有用,但它們本身並不能保證任意網頁查詢參數能在 App Store 全新安裝後存留。
跨平台架構差異:建構統一的參數合約
雖然 Android 和 iOS 使用不同的平台機制,但應用程式可以將復原的數值正規化為共用的應用程式層級架構。
例如:
{
"utm_source": "facebook",
"utm_medium": "paid_social",
"utm_campaign": "summer_sale",
"inviter_id": "USR_1024",
"promo_code": "WELCOME50"
}
此 JSON 物件代表一個應用程式定義的正規化參數合約,而非每個 OpoInstall SDK 版本保證的回傳格式。
應用程式應將這些欄位分為兩類:
- 行銷歸因:
utm_source、utm_medium和utm_campaign。 - 推薦與促銷行為:
inviter_id和promo_code。
行銷歸因可提交至授權的分析管道,而推薦與促銷行為則需在 App 核發任何利益前,進行獨立的伺服器端驗證。
情境負載快取在橋接商店轉跳中的角色
延遲還原服務通常需要對安裝情境進行有限度的保留。
適用的保留視窗、還原行為及配對可用性取決於所部署的服務、SDK 設定、網路狀況、平台限制與隱私要求。
工程團隊應衡量其自身安裝群體的還原結果,而非採信未經證實的準確度聲明。
隱私意識下的延遲情境架構與平台邊界
iOS 系統剪貼簿邊界:透明度橫幅與 UIPasteControl 意圖機制
部分過去的延遲深度連結實現曾使用系統剪貼簿在應用程式間傳遞臨時情境。
然而,iOS 對應用程式間的剪貼簿存取設有嚴格限制並對使用者公開。
- iOS 14: 讀取從其他應用程式複製的內容可能會觸發剪貼簿存取通知。
- iOS 16 及後續版本: 某些程式化的貼上操作可能觸發許可提示。如
UIPasteControl等控制項則支援使用者主動發起的貼上互動。
因此,剪貼簿存取不可被視為一種隱形或不受限的安裝參數傳輸通道。
使用與剪貼簿相關功能的應用程式,應確保互動符合使用者預期,並妥善處理許可遭拒或內容缺失的情況。
Android 前景剪貼簿限制:導航限制與自動清除
Android 同樣對剪貼簿存取設有限制。
- Android 10 及後續版本: 若應用程式未在前景情境中,剪貼簿存取將受限,並受平台定義的例外條款制約。
- Android 13 及後續版本: Android 為剪貼簿內容提供使用者介面回饋與自動清除機制。
應用程式不得假定可以靜默方式檢索剪貼簿數據,或該數據能無限期保留。
剪貼簿存取應為選擇性,且僅限於合適的使用者互動流程,不應被視為主要的歸因通道。
嚴格遵守 Apple 使用者隱私與數據使用政策:禁止裝置指紋識別
Apple 禁止透過裝置與環境特徵組合來識別使用者或裝置的指紋識別行為。
開發者不得透過 IP 位址、User Agent 資訊、顯示特徵或其他裝置訊號的組合,建構持久或機率性的識別碼,此舉將違反 Apple 政策。
取得 App Tracking Transparency (ATT) 許可並不代表禁止的指紋識別方法變得合規。
對於任何第三方延遲參數還原服務,開發者應審核該 SDK 實際的數據收集、配對行為、隱私披露及平台政策要求。
具備隱私意識的整合應優先考慮數據最小化、透明的使用者互動與符合平台規範的機制。
安全與信任邊界:管理參數生命週期與伺服器授權

所有還原的參數皆須視為未受信任的輸入 (Untrusted Input)。
行銷標籤與推薦 Token 可能來自可編輯的 URL 或未受信任的用戶端請求。它們的存在本身並不代表該活動合法,也不代表使用者有資格獲得獎勵。
應用程式應遵循以下原則:
- 列入白名單的數值: 僅接受明確支援的鍵值與有限的數值。
- 分離歸因與授權: UTM 數值可用於描述流量來源;但不得獨立核發促銷優惠。
- 時間限制的待處理記錄: 僅保留處理所需的資訊,並設定應用程式定義的過期視窗。
- 安全儲存: 使用適合待處理 Token 敏感度的儲存方式,例如 Android Keystore 或 iOS Keychain Services。
- 獨立的發放結果: 若記錄同時包含行銷與獎勵參數,應用程式必須獨立追蹤所需結果。
- 伺服器端獎勵驗證: 推薦點數、促銷折扣與類似權益必須經過已驗證的應用程式後端核准。
- 冪等獎勵申請: 後端應執行交易層級的冪等性與商業層級的唯一性限制,確保重複的用戶端請求無法重複領取一次性獎勵。
下方的程式碼展示了參數邊界與主應用程式的整合責任。它並不試圖提供一套完整的生產級儲存、分析或獎勵交易系統。
端到端實現:從網頁參數捕捉到原生還原
淨化網頁查詢參數:HTML 啟動、白名單解析與 Web JS SDK 設定
登陸頁面可擷取明確支援的查詢參數,並將其作為有限負載傳遞給 Web SDK。
以下範例演示:
- 從設定的來源載入 SDK。
- 保留正常的 HTTPS 下載降級連結。
- 僅接受支援的參數鍵。
- 拒絕不符合狹義字元與長度政策的數值。
- 將 SDK 下載動作連接至頁面的下載按鈕。
SDK 建構函式、設定鍵與方法皆為代表性範例。在發布或部署前,請務必依據實際部署的 OpoInstall Web SDK 版本核對簽章。
<!-- CODE_BLOCK_01: 代表性 HTML Bootstrap 與 Web SDK 整合 -->
<!-- 檔案: landing_page.html -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>應用程式下載</title>
</head>
<body>
<!-- 請替換為真實的 HTTPS 下載或商店導向 URL -->
<a
id="download-app-btn"
href="https://app.example.com/store-download"
>
下載 App
</a>
<!-- 請確認官方 CDN URL 與部署的 SDK 版本相符 -->
<script src="https://web.cdn.opoinstallcloud.com/openinstall.js"></script>
<script>
(function () {
"use strict";
function getSanitizedQueryParams() {
var allowedKeys = new Set([
"utm_source",
"utm_medium",
"utm_campaign",
"inviter_id",
"promo_code"
]);
var validPattern = /^[A-Za-z0-9_.-]{1,64}$/;
var result = Object.create(null);
try {
var query = new URLSearchParams(window.location.search);
query.forEach(function (value, key) {
if (
allowedKeys.has(key) &&
validPattern.test(value) &&
!Object.prototype.hasOwnProperty.call(result, key)
) {
result[key] = value;
}
});
} catch (error) {
console.warn("無法解析 URL 參數。");
return Object.create(null);
}
return result;
}
var downloadButton = document.getElementById(
"download-app-btn"
);
// 保留錨點 href 作為正常的瀏覽器降級選擇
if (!downloadButton) {
return;
}
if (typeof OpenInstall === "undefined") {
console.warn(
"Web SDK 無法使用;使用下載連結。"
);
return;
}
var customParams = getSanitizedQueryParams();
try {
new OpenInstall({
appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
data: customParams,
onready: function () {
var sdkClient = this;
downloadButton.addEventListener(
"click",
function (event) {
try {
// 代表性 SDK 方法。
// 請驗證其行為以及針對已部署版本的非同步失敗處理。
sdkClient.wakeupOrInstall();
event.preventDefault();
} catch (error) {
console.warn(
"SDK 動作失敗;瀏覽器將使用降級連結。"
);
// 請勿阻止預設的錨點動作
}
}
);
}
});
} catch (error) {
console.warn(
"Web SDK 初始化失敗;使用降級連結。"
);
}
})();
</script>
</body>
</html>
此數值政策僅接受字母、數字、底線、連字號與句點,長度上限為 64 個字元。
此參考實現排除了包含空格或其他字元的數值。有更廣泛 UTM 命名需求的團隊,應定義並測試適合特定欄位的 Schema,而非默默假設所有 URL 數值皆受支援。
同樣地,如 gclid 和 fbclid 等廣告點擊識別碼,雖被視為常見的網頁歸因概念,但並未包含在此範例白名單中。支援這些識別碼需要額外的數據收集、保留與隱私權審核。
當 SDK 無法載入或初始化時,降級連結錨點仍可使用。由於 SDK 操作可能發生非同步失敗,生產環境整合必須遵循 SDK 的錯誤處理與轉跳行為文件;單純的同步 try/catch 並不足以作為完整的非同步降級策略。
Android 原生實現:SDK 設定、參數驗證與主應用程式合約
安裝後,Android 應用程式可請求原生 SDK 支援的參數,並將其正規化為應用程式定義的記錄。
下方範例特意保持職責分離:
- OpoInstall SDK 於應用程式主進程中初始化。
- 原生回調提供備選負載。
- 應用程式驗證並正規化該負載。
- 應用程式透過主程式提供的儲存介面儲存記錄。
- 主程式提供的處理元件協調行銷歸因與獎勵授權。
- 應用程式僅在該元件回報已驗證的處理結果後,將記錄標記為完成。
注意: 主程式介面為應用程式定義的合約,而非 OpoInstall API。此範例要求主應用程式提供實現。它不包含不安全的降級儲存、模擬的分析傳遞或獎勵授權回應。
// CODE_BLOCK_02: 代表性 Android 整合 (Kotlin)
// 檔案: CustomApplication.kt 與 MainActivity.kt
//
// 請依據部署的 AAR 核對 OpoInstall 匯入與回調。
// 在處理參數前必須提供主程式實現。
package com.example.app
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.opoinstall.api.OpoInstall
import com.opoinstall.api.OpoData
import com.opoinstall.api.OpoError
import com.opoinstall.api.ResultCallBack
import org.json.JSONObject
import java.util.UUID
enum class ParamFetchState {
NOT_STARTED,
FETCHING,
RECEIVED,
APPLIED,
REJECTED,
NO_MATCH,
RETRYABLE_FAILURE
}
data class PendingAttributionRecord(
val utmSource: String?,
val utmMedium: String?,
val utmCampaign: String?,
val inviterId: String?,
val promoCode: String?,
val idempotencyKey: String,
val createdAt: Long,
val expiresAt: Long
) {
val hasAttributionData: Boolean
get() = listOf(
utmSource, utmMedium, utmCampaign
).any { !it.isNullOrEmpty() }
val hasRewardData: Boolean
get() = !inviterId.isNullOrEmpty() ||
!promoCode.isNullOrEmpty()
val hasAnyData: Boolean
get() = hasAttributionData || hasRewardData
fun isValid(): Boolean {
val now = System.currentTimeMillis()
val pattern = Regex("^[A-Za-z0-9_.-]{1,64}$")
val values = listOf(
utmSource, utmMedium, utmCampaign,
inviterId, promoCode
)
val validFields = values.all {
it == null || pattern.matches(it)
}
val validWindow =
expiresAt > createdAt &&
expiresAt - createdAt <= 48L * 60 * 60 * 1000 &&
now >= createdAt &&
now <= expiresAt
val validId = try {
UUID.fromString(idempotencyKey)
true
} catch (_: IllegalArgumentException) {
false
}
return hasAnyData &&
validFields &&
validWindow &&
validId
}
}
// 主程式定義的安全持久化合約。
// 實現必須區分缺失數據與儲存失敗,並在必要時處理帳戶綁定。
interface PendingTransactionStore {
fun save(record: PendingAttributionRecord): Boolean
fun load(): PendingAttributionRecord?
fun clear(): Boolean
}
// 主程式定義的協調器。負責獨立的遙測/獎勵確認、
// 冪等性、重試處理與交易身分檢查。
interface InstallParameterProcessor {
fun process(
record: PendingAttributionRecord,
onComplete: (ProcessingOutcome) -> Unit
)
}
enum class ProcessingOutcome {
COMPLETED,
REJECTED,
RETRYABLE_FAILURE
}
// 此為應用程式相依項目,而非 OpoInstall SDK API。
object HostAppDependencies {
var transactionStore: PendingTransactionStore? = null
var parameterProcessor: InstallParameterProcessor? = null
val isConfigured: Boolean
get() = transactionStore != null &&
parameterProcessor != null
}
class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
if (isMainProcess()) {
OpoInstall.initialize(this)
// 主程式須在此處使用實際的 DI/bootstrap 機制設定相依項目
//
// HostAppDependencies.transactionStore =
// hostProvidedProtectedStore
//
// HostAppDependencies.parameterProcessor =
// hostProvidedInstallParameterProcessor
}
}
private fun isMainProcess(): Boolean {
val manager = getSystemService(
Context.ACTIVITY_SERVICE
) as ActivityManager
val currentPid = Process.myPid()
return manager.runningAppProcesses?.any {
it.pid == currentPid &&
it.processName == packageName
} == true
}
}
class MainActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private var state = ParamFetchState.NOT_STARTED
private var activeRecord: PendingAttributionRecord? = null
private var processingInFlight = false
private val transactionTtlMs =
24L * 60 * 60 * 1000
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startParameterFlow()
}
private fun startParameterFlow() {
val store = HostAppDependencies.transactionStore
val processor = HostAppDependencies.parameterProcessor
if (store == null || processor == null) {
Log.w(
"OpoInstall",
"主程式相依項目無法使用;處理已暫停"
);
state = ParamFetchState.RETRYABLE_FAILURE
return
}
// 代表性記錄復原。
// 生產環境儲存必須區分讀取失敗與確認沒有待處理記錄的情況。
val existingRecord = try {
store.load()
} catch (error: Exception) {
state = ParamFetchState.RETRYABLE_FAILURE
return
}
if (existingRecord != null) {
if (existingRecord.isValid()) {
activeRecord = existingRecord
state = ParamFetchState.RECEIVED
processRecord(existingRecord)
} else {
// 無效或過期的記錄不予處理。
if (!store.clear()) {
state = ParamFetchState.RETRYABLE_FAILURE
return
}
requestInstallParameters()
}
} else {
requestInstallParameters()
}
}
private fun requestInstallParameters() {
if (!HostAppDependencies.isConfigured) {
state = ParamFetchState.RETRYABLE_FAILURE
return
}
if (state == ParamFetchState.FETCHING ||
state == ParamFetchState.APPLIED ||
state == ParamFetchState.NO_MATCH ||
processingInFlight
) {
return
}
state = ParamFetchState.FETCHING
// 代表性 SDK 回調。
// 請依據已部署的 AAR 核對簽章。
OpoInstall.getInstance().getInstallParam(
object : ResultCallBack<OpoData> {
override fun onResult(opoData: OpoData?) {
mainHandler.post {
if (state != ParamFetchState.FETCHING) {
return@post
}
val rawData: Any? = opoData?.data
val payload = when (rawData) {
is String -> rawData
is JSONObject -> rawData.toString()
is Map<*, *> -> try {
JSONObject(rawData).toString()
} catch (_: Exception) {
null
}
else -> null
}
val record = payload?.let {
parseAndSanitizeRawPayload(it)
}
if (record == null) {
// 未復原任何符合資格的數據。
// 這並不代表是自然安裝。
state = ParamFetchState.NO_MATCH
return@post
}
val store =
HostAppDependencies.transactionStore
if (store == null ||
!HostAppDependencies.isConfigured
) {
state = ParamFetchState.RETRYABLE_FAILURE
return@post
}
activeRecord = record
// 如果儲存失敗,請勿宣告已持久復原。
val saved = try {
store.save(record)
} catch (_: Exception) {
false
}
if (!saved) {
Log.w(
"OpoInstall",
"無法持久化待處理記錄"
);
state = ParamFetchState.RETRYABLE_FAILURE
return@post
}
state = ParamFetchState.RECEIVED
processRecord(record)
}
}
override fun onError(error: OpoError?) {
mainHandler.post {
if (state == ParamFetchState.FETCHING) {
Log.w(
"OpoInstall",
"安裝參數檢索失敗"
);
state = ParamFetchState.RETRYABLE_FAILURE
}
}
}
}
)
}
private fun parseAndSanitizeRawPayload(
rawJson: String
): PendingAttributionRecord? {
return try {
if (rawJson.length > 4096) return null
val json = JSONObject(rawJson)
val pattern = Regex("^[A-Za-z0-9_.-]{1,64}$")
fun field(key: String): String? =
(json.opt(key) as? String)?.takeIf {
pattern.matches(it)
}
val now = System.currentTimeMillis()
val record = PendingAttributionRecord(
utmSource = field("utm_source"),
utmMedium = field("utm_medium"),
utmCampaign = field("utm_campaign"),
inviterId = field("inviter_id"),
promoCode = field("promo_code"),
idempotencyKey = UUID.randomUUID().toString(),
createdAt = now,
expiresAt = now + transactionTtlMs
)
record.takeIf { it.isValid() }
} catch (_: Exception) {
null
}
}
private fun processRecord(
record: PendingAttributionRecord
) {
val processor = HostAppDependencies.parameterProcessor
val store = HostAppDependencies.transactionStore
if (processor == null || store == null ||
!record.isValid()
) {
state = ParamFetchState.RETRYABLE_FAILURE
return
}
if (processingInFlight) return
processingInFlight = true
// 主程式處理器必須確認所有要求的頻道特定結果後,才能回報 COMPLETED。
processor.process(record) { outcome ->
mainHandler.post {
// 忽略被取代或過期的交易。
if (activeRecord?.idempotencyKey !=
record.idempotencyKey
) {
return@post
}
processingInFlight = false
when (outcome) {
ProcessingOutcome.COMPLETED -> {
finalizeRecord(
record,
ParamFetchState.APPLIED
)
}
ProcessingOutcome.REJECTED -> {
finalizeRecord(
record,
ParamFetchState.REJECTED
)
}
ProcessingOutcome.RETRYABLE_FAILURE -> {
state =
ParamFetchState.RETRYABLE_FAILURE
}
}
}
}
}
private fun finalizeRecord(
record: PendingAttributionRecord,
terminalState: ParamFetchState
) {
val store = HostAppDependencies.transactionStore
?: run {
state = ParamFetchState.RETRYABLE_FAILURE
return
}
if (activeRecord?.idempotencyKey !=
record.idempotencyKey
) {
return
}
// 此範例將持久性的終端日誌記錄與原子清理工作留給主程式儲存。
// clear() 必須回報清理是否成功。
val cleared = try {
store.clear()
} catch (_: Exception) {
false
}
if (!cleared) {
state = ParamFetchState.RETRYABLE_FAILURE
return
}
activeRecord = null
state = terminalState
Log.i(
"OpoInstall",
"安裝參數處理已完成"
);
}
}
Android 範例特意不包含 PendingTransactionStore 或 InstallParameterProcessor 的預設實現。
主應用程式必須在呼叫流程前提供這些實現。
特別是,處理器不能僅因為記錄了 Log 或啟動了非同步請求就回報 COMPLETED。完成結果必須對應至實際負載的處理要求:
- 純 UTM 負載: 所需的歸因事件已可靠地被設定的分析系統或持久化事件佇列所接受。
- 純獎勵負載: 已驗證的後端回傳了已接受的終端授權結果。
- 混合 UTM 與獎勵負載: 兩個所需頻道均達到各自定義的結果,且其中一個頻道不可過早移除另一個頻道所需的工作。
若主應用程式需要跨進程復原,處理器與保護儲存區必須額外持久化足夠的頻道特定進度並保留穩定的交易識別碼。
上述 Activity 演示了整合邊界;它不能取代那些應用程式特有的實現。
iOS 原生實現:SDK 還原與應用程式整合合約
iOS 整合遵循相同的邊界。
原生 SDK 提供候選安裝參數。應用程式驗證負載,透過主程式提供的儲存元件持久化待處理記錄,並將商業處理委派給應用程式擁有的協調器。
程式碼為代表性範例,必須配合所安裝 OpoInstall iOS SDK 版本的模組名稱、委派協定與回調簽章進行調整。
// CODE_BLOCK_03: 代表性 iOS 整合 (Swift)
// 檔案: AppDelegate.swift
//
// 請依據已部署的 OpoInstall 架構版本核對模組與 SDK 方法。
import UIKit
import libOpoInstallSDK
enum ParamFetchState: String {
case notStarted = "NOT_STARTED"
case fetching = "FETCHING"
case received = "RECEIVED"
case applied = "APPLIED"
case rejected = "REJECTED"
case noMatch = "NO_MATCH"
case retryableFailure = "RETRYABLE_FAILURE"
}
struct PendingAttributionRecord: Codable {
let utmSource: String?
let utmMedium: String?
let utmCampaign: String?
let inviterId: String?
let promoCode: String?
let idempotencyKey: String
let createdAt: TimeInterval
let expiresAt: TimeInterval
var hasAttributionData: Bool {
return [
utmSource, utmMedium, utmCampaign
].contains { !$0.isNilOrEmpty }
}
var hasRewardData: Bool {
return !inviterId.isNilOrEmpty ||
!promoCode.isNilOrEmpty
}
var hasAnyData: Bool {
return hasAttributionData || hasRewardData
}
var isValid: Bool {
let now = Date().timeIntervalSince1970
let window = expiresAt - createdAt
let pattern = "^[A-Za-z0-9_.-]{1,64}$"
let fields = [
utmSource, utmMedium, utmCampaign,
inviterId, promoCode
]
let validFields = fields.allSatisfy { field in
guard let field = field else { return true }
return field.range(
of: pattern,
options: .regularExpression
) != nil
}
return hasAnyData &&
validFields &&
UUID(uuidString: idempotencyKey) != nil &&
window > 0 &&
window <= 48 * 60 * 60 &&
now >= createdAt &&
now <= expiresAt
}
}
private extension Optional where Wrapped == String {
var isNilOrEmpty: Bool {
return self?.isEmpty ?? true
}
}
// 主程式提供的受保護持久化合約。
protocol PendingTransactionStore {
func save(_ record: PendingAttributionRecord) -> Bool
func load() throws -> PendingAttributionRecord?
func clear() -> Bool
}
enum ProcessingOutcome {
case completed
case rejected
case retryableFailure
}
// 主程式提供的分析與獎勵處理協調器。
protocol InstallParameterProcessor {
func process(
record: PendingAttributionRecord,
completion: @escaping (ProcessingOutcome) -> Void
)
}
// 此為應用程式合約,而非 OpoInstall API。
struct HostAppDependencies {
static var transactionStore: PendingTransactionStore?
static var parameterProcessor: InstallParameterProcessor?
static var isConfigured: Bool {
return transactionStore != nil &&
parameterProcessor != nil
}
}
@UIApplicationMain
class AppDelegate:
UIResponder,
UIApplicationDelegate,
OpoInstallDelegate {
var window: UIWindow?
private var state: ParamFetchState = .notStarted
private var activeRecord: PendingAttributionRecord?
private var processingInFlight = false
private let transactionTTL: TimeInterval =
24 * 60 * 60
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 代表性 SDK 初始化。
OpoInstallSDK.initWith(self)
// 主程式必須在開始參數處理前註冊其實際的相依項目
//
// HostAppDependencies.transactionStore =
// hostProvidedProtectedStore
//
// HostAppDependencies.parameterProcessor =
// hostProvidedInstallParameterProcessor
startParameterFlow()
return true
}
private func startParameterFlow() {
guard HostAppDependencies.isConfigured,
let store = HostAppDependencies.transactionStore
else {
print(
"OpoInstall: 所需的主程式相依項目無法使用"
);
state = .retryableFailure
return
}
do {
if let saved = try store.load() {
if saved.isValid {
activeRecord = saved
state = .received
processRecord(saved)
return
}
guard store.clear() else {
state = .retryableFailure
return
}
}
} catch {
// 讀取錯誤不等於儲存為空。
state = .retryableFailure
return
}
requestInstallParameters()
}
private func requestInstallParameters() {
guard HostAppDependencies.isConfigured,
state != .fetching,
state != .applied,
state != .noMatch,
!processingInFlight
else {
return
}
guard let manager = OpoInstallSDK.defaultManager()
else {
state = .retryableFailure
return
}
state = .fetching
// 代表性回調簽章。
manager.getInstallParmsCompleted({
[weak self] (appData: OpoinstallData?) in
DispatchQueue.main.async {
guard let self = self,
self.state == .fetching
else {
return
}
let rawData: Any? = appData?.data
guard let record =
self.parseAndSanitizeRawPayload(rawData)
else {
// 未復原任何符合資格的自訂參數。
// 這並非自然流量的絕對證據。
self.state = .noMatch
return
}
guard HostAppDependencies.isConfigured,
let store =
HostAppDependencies.transactionStore
else {
self.state = .retryableFailure
return
}
self.activeRecord = record
guard store.save(record) else {
// 儲存失敗時,請勿宣告已持久化待處理記錄。
self.state = .retryableFailure
return
}
self.state = .received
self.processRecord(record)
}
})
}
private func parseAndSanitizeRawPayload(
_ rawData: Any?
) -> PendingAttributionRecord? {
guard let rawData = rawData else {
return nil
}
var dictionary: [String: Any]?
if let map = rawData as? [String: Any] {
dictionary = map
} else if let jsonString = rawData as? String,
jsonString.count <= 4096,
let data = jsonString.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(
with: data
) as? [String: Any] {
dictionary = parsed
}
guard let payload = dictionary else {
return nil
}
let pattern = "^[A-Za-z0-9_.-]{1,64}$"
func field(_ key: String) -> String? {
guard let value = payload[key] as? String,
value.range(
of: pattern,
options: .regularExpression
) != nil
else {
return nil
}
return value
}
let now = Date().timeIntervalSince1970
let record = PendingAttributionRecord(
utmSource: field("utm_source"),
utmMedium: field("utm_medium"),
utmCampaign: field("utm_campaign"),
inviterId: field("inviter_id"),
promoCode: field("promo_code"),
idempotencyKey: UUID().uuidString,
createdAt: now,
expiresAt: now + transactionTTL
)
return record.isValid ? record : nil
}
private func processRecord(
_ record: PendingAttributionRecord
) {
guard record.isValid,
let processor =
HostAppDependencies.parameterProcessor
else {
state = .retryableFailure
return
}
guard !processingInFlight else {
return
}
processingInFlight = true
// 主程式處理器必須獨立確認所需的歸因與獎勵結果。
processor.process(record: record) {
[weak self] outcome in
DispatchQueue.main.async {
guard let self = self,
self.activeRecord?.idempotencyKey ==
record.idempotencyKey
else {
return
}
self.processingInFlight = false
switch outcome {
case .completed:
self.finalizeRecord(
record,
terminalState: .applied
)
case .rejected:
self.finalizeRecord(
record,
terminalState: .rejected
)
case .retryableFailure:
self.state = .retryableFailure
}
}
}
}
private func finalizeRecord(
_ record: PendingAttributionRecord,
terminalState: ParamFetchState
) {
guard activeRecord?.idempotencyKey ==
record.idempotencyKey,
let store =
HostAppDependencies.transactionStore
else {
state = .retryableFailure
return
}
// 主程式儲存區必須協調應用程式可靠性需求所需的持久化完成與清理工作。
guard store.clear() else {
state = .retryableFailure
return
}
activeRecord = nil
state = terminalState
print(
"OpoInstall: 安裝參數處理已完成"
);
}
}
iOS 範例未提供預設的 UserDefaults 支援待處理交易儲存,也未提供自動回報成功的遙測用戶端。
主應用程式必須根據其安全、分析與帳戶管理需求提供儲存實現與參數處理器。
若應用程式處理具有獎勵性質的數值,處理器在伺服器端授權完成前,不得回傳 .completed。
若負載僅包含行銷數據,完成結果必須反映應用程式定義的分析接受標準或可靠的佇列要求。
對於混合記錄,處理器必須針對每個頻道保留未解決的工作,並確保過時的回調不會結束其他交易。
此代表性程式碼刻意省略了完整的重試排程器與持久化的多頻道交易日誌。這些需求屬於主應用程式的範疇,且必須針對其生命週期與後端進行測試。
生產環境整合邊界與驗證清單
在部署任何行動範例前,工程團隊應驗證以下條件。
| 整合邊界 | 生產環境驗證要求 |
|---|---|
| Web SDK | 確認已部署版本的 CDN 來源、建構函式設定、下載方法以及非同步失敗行為。 |
| 原生 SDK | 依據實際 Android AAR 或 iOS 架構確認套件/模組名稱、初始化要求、回調簽章與負載類型。 |
| 輸入 Schema | 確認支援的參數名稱、數值長度、字元規則以及對畸形或重複鍵的處理方式。 |
| 受保護儲存 | 使用具備適當保護、過期機制與錯誤回報的主應用程式提供之儲存實現。 |
| 行銷遙測 | 確認處理完成代表分析事件已可靠被接受或持久化佇列,而非僅記錄於 Log。 |
| 獎勵授權 | 使用已驗證的應用程式後端驗證;切勿僅憑用戶端提供的參數核發獎勵。 |
| 混合負載 | 在每個所需的結果解析前,持續保留待處理的歸因與獎勵工作。 |
| 冪等性 | 維護重試期間穩定的交易識別碼,並於伺服器端強制執行獎勵唯一性。 |
| 正式發布版本 | 使用實際 SDK 組件驗證 Android 混淆編譯與 iOS 組件。 |
| 隱私審核 | 確認 SDK 實際的數據收集與配對行為符合平台與法規要求。 |
上述為整合驗收標準,並非宣告參考程式碼或特定 SDK 版本已通過生產環境測試。
參數傳遞策略比較矩陣
參數還原策略的方法論分析
行動獲取系統使用不同方法在網頁瀏覽與原生應用程式間保留推薦與歸因情境。
適合的方法取決於目標平台、所需的參數類型、可接受的使用者摩擦感、數據敏感度與可用的平台機制。
| 參數策略 | 還原覆蓋範圍 | 使用者摩擦 | 隱私/政策考量 | 是否需後端授權與冪等性 |
|---|---|---|---|---|
| 手動輸入優惠代碼 | 取決於使用者參與度與代碼可用性。 | 高;使用者須手動輸入代碼。 | 無需裝置配對,但仍適用一般數據保護規則。 | 是,用於獎勵兌換。 |
| Google Play Install Referrer | 支援 Android 流程的 Google Play 安裝。 | 成功實現時摩擦極小。 | 官方商店支援傳輸;下游處理與數據使用仍須審核。 | 是,當參照值觸發獎勵時。 |
| OpoInstall 參數還原 | 取決於部署的 SDK 行為、入口路徑、平台與允許的配對機制。 | 還原成功時摩擦極小。 | 需評估 SDK 數據行為、權限與平台規則。 | 是,用於獎勵參數。 |
評估準確度、隱私合規、使用者摩擦與安全邊界
手動輸入優惠代碼在技術上易於理解,但可能增加使用者摩擦。
Google Play Install Referrer 提供了原生、由商店支援的 Android 推薦管道。
第三方參數還原 SDK 可提供符合應用程式層級整合的延遲情境層。然而,其實際的傳遞覆蓋率、技術機制與隱私特徵,必須透過部署的 SDK 版本進行評估。
無論採用何種還原方法,應用程式皆須區分「收到參數」與「核發商業行為」之間的差異。
疑難排解:參數還原、狀態管理與 SDK 整合錯誤
管理用戶端狀態轉跳:處理無匹配、重試與終端結果

參數還原工作流程可透過七個邏輯狀態描述:
| 狀態 | 定義 |
|---|---|
NOT_STARTED |
尚未開始任何檢索嘗試。 |
FETCHING |
應用程式正在請求安裝參數。 |
RECEIVED |
已收到候選負載並接受進行處理。 |
APPLIED |
主程式處理器回報此記錄的所有必要結果均已完成。 |
REJECTED |
主程式處理器回報明確的終端拒絕。 |
NO_MATCH |
檢索嘗試未產生符合資格的自訂參數。 |
RETRYABLE_FAILURE |
流程暫時無法繼續,可依據控管政策進行重試。 |
這些狀態描述了應用程式層級的結果。它們無法取代主程式在分析、獎勵、儲存與重試方面的持久化協調。
特別是,僅行銷性質的記錄可在無需獎勵授權的情況下完成,而具備獎勵性質的記錄則必須獲得相應的伺服器回應。
NO_MATCH 結果不應被視為該安裝屬於自然流量的明確證明。
妥善解析參數請求超時與網路延遲
參數還原可能受網路連線、回調時間、App 啟動順序或後端服務不可用影響。
應用程式應:
- 維持非同步網路運作。
- 避免在安裝參數檢索期間阻塞主要使用者介面。
- 區分 SDK 管理器不可用與參數結果為空的情況。
- 將本地持久化錯誤視為處理失敗,而非確認無記錄存在的依據。
- 依據有限度的應用程式政策進行重試,而非在每次前景切換時反覆發送請求。
- 在必須重試時保留現有的待處理記錄。
任何自動重試的實現都應防止重複請求,並拒絕屬於已被取代交易的回調。
管理 App 覆蓋安裝與過期的參數緩衝區
參數還原通常針對符合資格安裝後的首次啟動,但應用程式更新與重複啟動可能暴露先前儲存的本地狀態。
工程團隊應定義何時具備還原嘗試資格、如何識別已消耗的記錄,以及待處理數據如何過期。
對於涉及獎勵的參數,伺服器端的冪等性至關重要。僅憑用戶端標記不足以防止獎勵重複核發。
當應用程式在本地維護待處理交易時,僅在所需的處理結果已持久解決後,才應移除該記錄。
防止 Android 多進程重新初始化問題
具備多進程的 Android 應用程式可能多次執行 Application.onCreate()。
若原生 SDK 僅旨在主要應用程式進程中初始化,開發者應在初始化前驗證目前進程。
Android 範例演示了一種進程名稱比較策略。生產應用程式應依據其支援的 Android 版本與進程架構核對該方法。
偵錯用戶端回調簽章不符與程式碼縮減混淆規則
第三方 SDK 的套件名稱、回調介面與類別宣告可能隨發布版本改變。
當整合失敗時,開發者應驗證實際部署的 SDK,而非假設範例介面保持不變。
對於 Android 正式發布編譯:
- 優先使用隨 SDK 原始組件提供的 R8 / ProGuard 規則。
- 避免在無證據情況下使用廣泛的套件層級
-keep或-dontwarn規則。 - 在添加窄化補充規則前,檢視實際的反射要求。
- 在混淆編譯配置下測試安裝參數回調。
本指南中的程式碼僅為代表性整合範例,在無對應編譯與執行階段證據的情況下,不可宣稱為已通過正式版本驗證。
常見問題 (FAQ)
當使用者透過 App Store 安裝 App 時,追蹤參數如何保留?
iOS 與 Android 上的剪貼簿負載有哪些隱私權限制?
追蹤參數能否攜帶巢狀 JSON 負載以實現動態 App 內導航?
總結與決策框架
將網頁點擊串接至行動 App 商店安裝,需要一種尊重瀏覽器、App 商店與原生應用程式技術邊界的參數還原方法。
對於 Android,Google Play Install Referrer API 為支援的安裝流程提供了官方參照機制。對於 iOS,任意網頁參數無法透過 App Store 自動傳遞,因此延遲還原需要採取獨立驗證、具備隱私意識的方法。
實用的 OpoInstall 整合可擷取列入白名單的登陸頁面參數,並透過原生 SDK 請求符合資格的情境。應用程式隨後必須正規化並驗證所恢復的數據,再將其用於行銷歸因、導航或促銷工作流程。
關鍵工程區分在於參數還原 (Parameter Restoration) 與應用程式端處理 (Application-side processing)。成功復原參數並不保證分析資料已傳遞、獎勵資格有效或已獲得成功授權。
參考程式碼演示了 SDK 整合邊界與應用程式定義的合約。生產環境團隊必須提供自己的受保護儲存、可靠的分析處理、伺服器端權益驗證以及生命週期協調。
若要進一步探索供應商整合要求,請參考 OpoInstall SDK 文件 或 OpoInstall 開發者註冊頁面。
相關資源
概念: App 參數安裝、Google Play 安裝參照、延遲參數還原、歸因遙測、伺服器授權邊界。
技術: Web JS SDK、原生行動 SDK、URL 查詢參數、Android Keystore、iOS Keychain Services。
標準: IETF RFC 3986 URI 語法、OWASP 行動應用程式安全驗證標準 (MASVS)、W3C 剪貼簿 API 與事件。
API: OpoInstall 原生安裝參數 API、Google Play 安裝參照 API、Apple UIPasteboard API、Android 剪貼簿 API。
官方文件與參考資料:
Share this article


