วิธีส่งพารามิเตอร์การติดตามจากเว็บไปยังการติดตั้งแอป

opoinstall
2026-10-10
5 min read

จะส่งพารามิเตอร์การติดตามจากเว็บไซต์บนมือถือไปยังการติดตั้งผ่านสโตร์ได้อย่างไร? การส่งพารามิเตอร์การติดตามจากเว็บไซต์บนมือถือไปยังแอปที่เพิ่งติดตั้งใหม่เกี่ยวข้องกับการโหลด OpoInstall Web JS SDK, การดึงพารามิเตอร์ URL ที่ได้รับอนุญาต และการกู้คืนข้อมูลการระบุที่มา (attribution) ที่รองรับผ่าน Native SDK เมื่อแอปเปิดใช้งานครั้งแรก

การเชื่อมโยงพารามิเตอร์ผ่านการดาวน์โหลดจากแอปสโตร์คือกระบวนการรักษาพารามิเตอร์ URL ของเว็บที่เข้าเงื่อนไขเมื่อผู้ใช้เปลี่ยนผ่านจากหน้า Landing Page บนมือถือไปยังแอปสโตร์และเปิดแอปที่เพิ่งติดตั้งใหม่ Web SDK จะจับบริบทของแคมเปญหรือการอ้างอิง ในขณะที่กลไกที่แพลตฟอร์มรองรับหรือบริการกู้คืนแบบดีเฟอร์จะทำให้พารามิเตอร์ที่เกี่ยวข้องพร้อมใช้งานสำหรับแอปมือถือ การกู้คืนขึ้นอยู่กับแพลตฟอร์ม การกำหนดค่า SDK สัญญาณการจับคู่ที่มีอยู่ และข้อกำหนดด้านความเป็นส่วนตัวที่เกี่ยวข้อง

คำศัพท์ คำนิยาม เอนทิตีที่เกี่ยวข้อง บทบาทตามเจตนาการค้นหา
พารามิเตอร์การติดตาม (Tracking Parameters) คู่คีย์-ค่าที่ต่อท้าย URL เพื่อระบุบริบทของทราฟฟิก แหล่งที่มาของแคมเปญ หรือข้อมูลการอ้างอิง URL Query String เชิงข้อมูล / เชิงเทคนิค
การติดตั้งพารามิเตอร์แอป (App Parameter Installation) เวิร์กโฟลว์การกู้คืนพารามิเตอร์แบบดีเฟอร์ที่ทำให้บริบทของเว็บที่เข้าเงื่อนไขพร้อมใช้งานหลังจากการติดตั้ง Native Mobile SDK เชิงเทคนิค / เชิงข้อมูล
Google Play Install Referrer API อย่างเป็นทางการของ Android สำหรับดึงข้อมูลเมตาการอ้างอิงที่เกี่ยวข้องกับการติดตั้งผ่าน Google Play Android SDK เชิงเทคนิค

ช่องว่างการระบุที่มาจากเว็บสู่สโตร์: อุปสรรคของแพลตฟอร์มในแต่ละแอปสโตร์

พารามิเตอร์การติดตามของเว็บไม่สามารถข้ามขอบเขตการติดตั้งของแอปสโตร์ได้โดยอัตโนมัติ

อุปสรรคของการตัดการเชื่อมต่อ: ระบบ Sandbox ของระบบปฏิบัติการแยกบริบทของเบราว์เซอร์อย่างไร

การวิเคราะห์เว็บโดยทั่วไปจะใช้ URL Query Strings รวมถึงแท็ก UTM (utm_source, utm_medium, utm_campaign), ตัวระบุการคลิกโฆษณา เช่น gclid และ fbclid และพารามิเตอร์การอ้างอิงแบบกำหนดเอง

ภายในเว็บไซต์ที่เข้าร่วม API ของเบราว์เซอร์และระบบวิเคราะห์บุคคลที่หนึ่งสามารถเก็บค่าเหล่านี้เพื่อการวิเคราะห์และเวิร์กโฟลว์แคมเปญที่ได้รับอนุญาต

ความท้าทายจะเกิดขึ้นเมื่อผู้ใช้ออกจากเบราว์เซอร์และเข้าสู่กระบวนการติดตั้งผ่านแอปสโตร์ เบราว์เซอร์และแอปมือถือที่ติดตั้งใหม่ทำงานในสภาพแวดล้อมที่แยกจากกัน คุกกี้ของเบราว์เซอร์, Session Storage และพารามิเตอร์ URL ของหน้าเว็บจะไม่พร้อมใช้งานภายในแอปพลิเคชันโดยอัตโนมัติหลังจากการติดตั้ง

ด้วยเหตุนี้ นักพัฒนาจึงจำเป็นต้องมีกลไกการอ้างอิงที่รองรับโดยสโตร์หรือเวิร์กโฟลว์การกู้คืนพารามิเตอร์แบบดีเฟอร์เพื่อเชื่อมต่อบริบทก่อนการติดตั้งที่เข้าเงื่อนไขเข้ากับเซสชันแอปหลังการติดตั้ง

ข้อมูลเฉพาะของแพลตฟอร์ม: การนำข้อมูลเข้าของ Apple App Store เทียบกับ Google Play Store

Android และ iOS ใช้กลไกที่แตกต่างกันก่อนที่พารามิเตอร์จะถูกทำให้เป็นมาตรฐานในสัญญาแอปเดียว

แอปสโตร์บนมือถือรองรับวิธีการส่งบริบทการอ้างอิงที่แตกต่างกัน

  • Apple App Store (iOS): การติดตั้งผ่าน App Store มาตรฐานจะไม่ส่งพารามิเตอร์ Query ของ URL ภายนอกโดยพลการไปยังสภาพแวดล้อมการเปิดใช้งานครั้งแรกของแอปที่ติดตั้งใหม่โดยอัตโนมัติ บริการกู้คืนแบบดีเฟอร์หรือเวิร์กโฟลว์ที่มีผู้ใช้เป็นตัวกลางอาจช่วยกู้คืนบริบทที่เข้าเงื่อนไขได้ โดยอยู่ภายใต้ข้อกำหนดด้านความเป็นส่วนตัวของ Apple
  • Google Play Store (Android): Google มี Google Play Install Referrer API อย่างเป็นทางการ ในกรณีที่มีการใช้ขั้นตอนการอ้างอิงของ Google Play ที่สร้างขึ้นอย่างถูกต้อง แอปที่ติดตั้งจะสามารถดึงข้อมูลเมตาการอ้างอิงที่รองรับ รวมถึงสตริงการอ้างอิงและเวลาที่เกี่ยวข้อง

กลไกเหล่านี้ไม่ควรถูกมองว่าสามารถใช้แทนกันได้ Google Play มีอินเทอร์เฟซการอ้างอิงระดับสโตร์อย่างเป็นทางการ ในขณะที่การกู้คืนพารามิเตอร์แบบดีเฟอร์โดยพลการบน iOS จำเป็นต้องมีกลไกที่ออกแบบแยกต่างหากและเป็นไปตามนโยบาย

ผลกระทบทางธุรกิจจากการที่ทราฟฟิกการติดตั้งไม่ถูกระบุที่มาต่อการคำนวณผลตอบแทนทางการตลาด (ROI)

หากพารามิเตอร์ของแคมเปญสูญหายระหว่างการเข้าชมเว็บและการติดตั้งแอป ระบบวิเคราะห์อาจไม่สามารถเชื่อมโยงการติดตั้งเข้ากับแคมเปญต้นทางได้อย่างถูกต้อง

สิ่งนี้อาจนำไปสู่การติดตั้งที่ไม่ได้ระบุที่มา, การรายงานการได้มาซึ่งผู้ใช้ที่ไม่สมบูรณ์, และการคำนวณผลตอบแทนจากการลงทุนทางการตลาด (ROI) หรือต้นทุนการได้มาซึ่งลูกค้า (CAC) ที่ไม่แม่นยำ

การสูญหายของพารามิเตอร์อาจส่งผลต่อประสบการณ์การเริ่มต้นใช้งาน ตัวอย่างเช่น ลิงก์การอ้างอิงของเว็บอาจรวมตัวระบุผู้แนะนำหรือรหัสโปรโมชั่น:

https://example.com/download?inviter_id=USR_1024
https://example.com/download?promo_code=WELCOME50

หากแอปพลิเคชันไม่สามารถกู้คืนค่าเหล่านี้ได้ ผู้ใช้อาจจำเป็นต้องกรอกข้อมูลด้วยตนเอง

อย่างไรก็ตาม การกู้คืนค่าการอ้างอิงหรือโปรโมชั่นไม่ได้เป็นการอนุญาตให้ได้รับรางวัลโดยอัตโนมัติ แบ็กเอนด์ของแอปพลิเคชันต้องตรวจสอบสิทธิ์, ขีดจำกัดการแลกรับ, และกฎทางธุรกิจที่เกี่ยวข้องอย่างเป็นอิสระ

กลไกพารามิเตอร์ที่รองรับโดยสโตร์เทียบกับแบบดีเฟอร์: Apple App Store เทียบกับ Google Play

กลไกของ Google Play Store: การใช้ Native Install Referrer API

บน Android, Google Play Install Referrer API นำเสนอกลไกอย่างเป็นทางการสำหรับการดึงข้อมูลเมตาการอ้างอิงการติดตั้งที่รองรับ

URL การดาวน์โหลดของ Google Play สามารถรวมพารามิเตอร์ referrer ที่เข้ารหัสไว้:

https://play.google.com/store/apps/details?id=com.example.app&referrer=utm_source%3Dfacebook%26inviter_id%3DUSR_1024

ใน URL ตัวอย่างนี้ ค่า referrer ที่เข้ารหัสจะแสดงถึง:

utm_source=facebook&inviter_id=USR_1024

หลังจากการติดตั้ง แอป Android แบบเนทีฟสามารถใช้ Install Referrer Client เพื่อดึงข้อมูลการอ้างอิงที่มีจาก Google Play

ไม่ว่า OpoInstall Web SDK เวอร์ชันเฉพาะจะสร้าง, ปรับเปลี่ยน, หรือจัดการ URL ดังกล่าวโดยอัตโนมัติหรือไม่นั้น จะต้องตรวจสอบกับเอกสารประกอบและการทำงานที่ใช้งานจริง การมีอยู่ของ API อย่างเป็นทางการของ Google ไม่ได้เป็นการยืนยันการนำไปใช้งานภายในของ SDK ของบุคคลที่สาม

ขอบเขตของ Apple App Store: การจัดการการเปลี่ยนเส้นทางสโตร์มาตรฐานโดยไม่มี Query Parameters

Apple App Store ไม่ส่งพารามิเตอร์ Query ของเว็บโดยพลการไปยัง Callback การเปิดใช้งานครั้งแรกของแอปที่ติดตั้งใหม่โดยตรง

เวิร์กโฟลว์การกู้คืนแบบดีเฟอร์จึงอาจเกี่ยวข้องกับการเก็บข้อมูลแคมเปญที่ได้รับอนุญาตก่อนการเปลี่ยนเส้นทางไปยังสโตร์ และการร้องขอบริบทที่เกี่ยวข้องเมื่อแอปเนทีฟเปิดใช้งาน

สำหรับเวิร์กโฟลว์ที่ใช้ SDK นักพัฒนาต้องแยกแยะระหว่างฟังก์ชันการทำงานที่ผู้ขายระบุไว้กับวิธีการจับคู่ที่ใช้โดยเวอร์ชันของ SDK ที่ติดตั้ง

วิธีการจับคู่หรือการเก็บข้อมูลใดๆ ต้องเป็นไปตามนโยบายความเป็นส่วนตัวของ Apple โดยเฉพาะอย่างยิ่ง การดำเนินการระบุที่มาจะต้องไม่พึ่งพาการทำลายนิ้วมืออุปกรณ์ (fingerprinting) ที่ถูกห้าม หรือสันนิษฐานว่าการอนุญาต ATT จะทำให้วิธีการจับคู่ที่ปกติถูกห้ามนั้นสามารถยอมรับได้

Universal Links มีประโยชน์สำหรับการกำหนดเส้นทางลิงก์ที่รองรับไปยังแอปที่ติดตั้งไว้แล้ว แต่ลิงก์เหล่านั้นไม่ได้เป็นตัวรับประกันว่าพารามิเตอร์ Query ของเว็บโดยพลการจะคงอยู่หลังจากการติดตั้งผ่าน App Store ใหม่

ความแตกต่างทางสถาปัตยกรรมข้ามแพลตฟอร์ม: การสร้างสัญญาพารามิเตอร์แบบรวมศูนย์

แม้ว่า Android และ iOS จะใช้กลไกแพลตฟอร์มที่แตกต่างกัน แต่แอปพลิเคชันสามารถทำให้ค่าที่กู้คืนมาเป็นมาตรฐานใน Schema ระดับแอปพลิเคชันที่ใช้ร่วมกันได้

ตัวอย่างเช่น:

{
  "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

การระบุที่มาทางการตลาดสามารถส่งไปยังไปป์ไลน์การวิเคราะห์ที่ได้รับอนุญาต ส่วนการดำเนินการอ้างอิงและโปรโมชั่นจำเป็นต้องมีการตรวจสอบสิทธิ์จากเซิร์ฟเวอร์ก่อนที่แอปพลิเคชันจะให้สิทธิประโยชน์ใดๆ

บทบาทของการแคชข้อมูลบริบทในการเชื่อมต่อการเปลี่ยนเส้นทางสโตร์

โดยทั่วไปบริการกู้คืนแบบดีเฟอร์จำเป็นต้องมีระยะเวลาการเก็บรักษาที่จำกัดสำหรับบริบทการติดตั้ง

หน้าต่างการเก็บรักษา, พฤติกรรมการกู้คืน, และความพร้อมใช้งานของการจับคู่ ขึ้นอยู่กับบริการที่ติดตั้ง, การกำหนดค่า SDK, สภาวะเครือข่าย, ข้อจำกัดของแพลตฟอร์ม, และข้อกำหนดด้านความเป็นส่วนตัว

ทีมวิศวกรรมควรวัดผลลัพธ์การกู้คืนโดยใช้กลุ่มผู้ใช้ที่ติดตั้งของตนเอง แทนที่จะยอมรับคำกล่าวอ้างเรื่องความแม่นยำที่ไม่มีหลักฐานรองรับ

สถาปัตยกรรมบริบทแบบดีเฟอร์ที่คำนึงถึงความเป็นส่วนตัวและขอบเขตของแพลตฟอร์ม

ขอบเขต Pasteboard ของระบบบน iOS: แบนเนอร์โปร่งใสและกลไกความตั้งใจของ UIPasteControl

การนำ Deferred Linking ไปใช้งานในอดีตบางกรณีใช้ Pasteboard ของระบบเพื่อส่งบริบทชั่วคราวระหว่างแอปพลิเคชัน

อย่างไรก็ตาม iOS จำกัดและเปิดเผยการเข้าถึง Pasteboard ข้ามแอปพลิเคชันต่อผู้ใช้

  • iOS 14: การอ่านเนื้อหาที่คัดลอกมาจากแอปอื่นอาจกระตุ้นการแจ้งเตือนการเข้าถึง Pasteboard
  • iOS 16 และใหม่กว่า: การดำเนินการวางข้อมูลแบบโปรแกรมบางอย่างอาจกระตุ้นการแจ้งเตือนขออนุญาต ตัวควบคุมเช่น UIPasteControl รองรับการโต้ตอบการวางข้อมูลที่ผู้ใช้เริ่มต้นเอง

ด้วยเหตุนี้ การเข้าถึง Pasteboard ทั่วไปจึงต้องไม่ถูกปฏิบัติในฐานะช่องทางการขนส่งพารามิเตอร์การติดตั้งที่มองไม่เห็นหรือไม่มีข้อจำกัด

แอปพลิเคชันที่ใช้ฟังก์ชันที่เกี่ยวข้องกับ Pasteboard ควรทำให้การโต้ตอบเหมาะสมกับความคาดหวังของผู้ใช้และจัดการกับการปฏิเสธการอนุญาตหรือเนื้อหาที่ขาดหายไปอย่างราบรื่น

ข้อจำกัดของคลิปบอร์ดเบื้องหน้าบน Android: ข้อจำกัดในการนำทางและการล้างข้อมูลอัตโนมัติ

Android ยังใช้ข้อจำกัดในการเข้าถึงคลิปบอร์ด

  • Android 10 และใหม่กว่า: การเข้าถึงคลิปบอร์ดถูกจำกัดสำหรับแอปพลิเคชันที่ไม่ได้อยู่ในบริบทเบื้องหน้าที่ได้รับอนุญาต โดยขึ้นอยู่กับข้อยกเว้นที่กำหนดโดยแพลตฟอร์ม
  • Android 13 และใหม่กว่า: Android จัดให้มีข้อเสนอแนะเกี่ยวกับส่วนต่อประสานผู้ใช้ที่เกี่ยวข้องกับคลิปบอร์ดและพฤติกรรมการล้างข้อมูลอัตโนมัติสำหรับเนื้อหาในคลิปบอร์ด

แอปพลิเคชันต้องไม่สันนิษฐานว่าข้อมูลคลิปบอร์ดสามารถดึงข้อมูลได้อย่างเงียบๆ หรือจะยังคงใช้งานได้ตลอดไป

การเข้าถึงคลิปบอร์ดควรเป็นทางเลือกและจำกัดเฉพาะเวิร์กโฟลว์ที่เหมาะสมกับผู้ใช้ แทนที่จะทำหน้าที่เป็นช่องทางการระบุที่มาหลักที่สันนิษฐานไว้

การปฏิบัติตามนโยบายความเป็นส่วนตัวและการใช้ข้อมูลของผู้ใช้ของ Apple อย่างเคร่งครัด: การห้ามทำลายนิ้วมืออุปกรณ์

Apple ห้ามการทำลายนิ้วมืออุปกรณ์ (device fingerprinting) ที่ใช้เพื่อระบุตัวตนผู้ใช้หรืออุปกรณ์ผ่านการรวมกันของลักษณะอุปกรณ์และสภาพแวดล้อม

การนำไปใช้งานจะต้องไม่สร้างตัวระบุที่คงอยู่หรือเป็นไปตามความน่าจะเป็นจากการรวมกันของที่อยู่ IP, ข้อมูล User Agent, ลักษณะการแสดงผล, หรือสัญญาณอุปกรณ์อื่นๆ ที่เป็นการละเมิดนโยบายของ Apple

การให้สิทธิ์ App Tracking Transparency ไม่ได้ทำให้อุปกรณ์ที่ใช้วิธีการทำลายนิ้วมืออุปกรณ์ที่ถูกห้ามนั้นสามารถยอมรับได้

สำหรับบริการกู้คืนพารามิเตอร์แบบดีเฟอร์ของบุคคลที่สาม นักพัฒนาควรตรวจสอบการเก็บข้อมูลจริง, พฤติกรรมการจับคู่, การเปิดเผยความเป็นส่วนตัว, และข้อกำหนดนโยบายแพลตฟอร์มของ SDK ที่ใช้งาน

การบูรณาการที่คำนึงถึงความเป็นส่วนตัวควรให้ความสำคัญกับการลดข้อมูลให้น้อยที่สุด, การโต้ตอบกับผู้ใช้ที่โปร่งใส, และกลไกแพลตฟอร์มที่รองรับ

ขอบเขตความปลอดภัยและความน่าเชื่อถือ: การจัดการวงจรชีวิตของพารามิเตอร์และการตรวจสอบสิทธิ์เซิร์ฟเวอร์

พารามิเตอร์การติดตั้งที่กู้คืนต้องผ่านการวิเคราะห์แยกต่างหากและการประมวลผลการตรวจสอบสิทธิ์บนแบ็กเอนด์

พารามิเตอร์ที่กู้คืนทั้งหมดต้องได้รับการปฏิบัติเสมือนเป็น ข้อมูลที่ไม่น่าเชื่อถือ (untrusted input)

แท็กทางการตลาดและโทเค็นการอ้างอิงอาจมาจาก URL ที่แก้ไขได้หรือคำขอจากไคลเอนต์ที่ไม่น่าเชื่อถือ การมีอยู่เพียงอย่างเดียวไม่ได้พิสูจน์ว่าแคมเปญนั้นถูกต้องตามกฎหมายหรือผู้ใช้มีสิทธิ์ได้รับรางวัล

แอปพลิเคชันควรปฏิบัติตามหลักการเหล่านี้:

  • ค่าที่ได้รับอนุญาต (Allowlisted values): ยอมรับเฉพาะคีย์ที่รองรับอย่างชัดเจนและค่าที่มีขอบเขตชัดเจนเท่านั้น
  • แยกการระบุที่มาจากการตรวจสอบสิทธิ์: ค่า UTM อาจอธิบายที่มาของทราฟฟิก แต่จะต้องไม่ใช้ตรวจสอบสิทธิ์เพื่อรับสิทธิประโยชน์ทางการตลาดโดยอิสระ
  • บันทึกที่ค้างอยู่แบบจำกัดเวลา: เก็บรักษาเฉพาะข้อมูลที่จำเป็นสำหรับการประมวลผล โดยอยู่ภายใต้หน้าต่างหมดอายุที่แอปพลิเคชันกำหนด
  • การจัดเก็บที่ได้รับการป้องกัน: ใช้การจัดเก็บที่เหมาะสมกับความละเอียดอ่อนของโทเค็นที่ค้างอยู่ เช่น การออกแบบที่ใช้ Android Keystore หรือ iOS Keychain Services ตามความเหมาะสม
  • ผลลัพธ์การส่งมอบที่เป็นอิสระ: สำหรับบันทึกที่มีทั้งพารามิเตอร์ทางการตลาดและรางวัล แอปพลิเคชันต้องติดตามผลลัพธ์ที่ต้องการแยกกัน
  • การตรวจสอบสิทธิ์รางวัลฝั่งเซิร์ฟเวอร์: เครดิตการอ้างอิง, ส่วนลดโปรโมชั่น, และสิทธิประโยชน์ที่คล้ายกันจะต้องได้รับอนุมัติจากแบ็กเอนด์ของแอปพลิเคชันที่ผ่านการตรวจสอบสิทธิ์แล้ว
  • การประยุกต์ใช้รางวัลแบบ Idempotent: แบ็กเอนด์ควรบังคับใช้ความไม่เปลี่ยนแปลง (idempotency) ในระดับธุรกรรมและข้อจำกัดความเป็นเอกลักษณ์ในระดับธุรกิจ เพื่อไม่ให้คำขอจากไคลเอนต์ที่ซ้ำกันสามารถรับรางวัลซ้ำซ้อนได้

โค้ดด้านล่างแสดงให้เห็นถึงขอบเขตของพารามิเตอร์และความรับผิดชอบในการบูรณาการของแอปพลิเคชันเจ้าบ้าน โดย ไม่ พยายามจัดเตรียมระบบการจัดเก็บข้อมูล, การวิเคราะห์, หรือระบบธุรกรรมรางวัลระดับการใช้งานจริงที่สมบูรณ์

การบูรณาการแบบ End-to-End: การจับพารามิเตอร์ของเว็บสู่การกู้คืนในแอปเนทีฟ

การทำความสะอาดพารามิเตอร์ Query ของเว็บ: การบูตสแตรป HTML, การแยกวิเคราะห์ Allowlist และการตั้งค่า Web JS SDK

หน้า Landing Page สามารถจับพารามิเตอร์ Query ที่รองรับอย่างชัดเจนและส่งไปยัง Web SDK ในรูปแบบเพย์โหลดที่มีขอบเขต

ตัวอย่างต่อไปนี้แสดงถึง:

  • การโหลด SDK จากแหล่งที่กำหนดค่าไว้
  • การเก็บรักษาลิงก์สำรอง HTTPS สำหรับการดาวน์โหลดตามปกติ
  • การยอมรับเฉพาะคีย์พารามิเตอร์ที่รองรับ
  • การปฏิเสธค่าที่อยู่นอกนโยบายตัวอักษรและความยาวที่จำกัด
  • การเชื่อมต่อการดำเนินการดาวน์โหลดของ SDK เข้ากับปุ่มดาวน์โหลดของหน้าเว็บ

ตัวสร้าง SDK, คีย์การกำหนดค่า และวิธีการนั้นเป็น ตัวอย่างเท่านั้น ตรวจสอบลายเซ็นกับ OpoInstall Web SDK รุ่นที่ใช้งานจริงก่อนเผยแพร่หรือใช้งาน

<!-- CODE_BLOCK_01: ตัวอย่าง HTML Bootstrap และการบูรณาการ Web SDK -->
<!-- File: landing_page.html -->

<!DOCTYPE html>
<html lang="th">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ดาวน์โหลดแอปพลิเคชัน</title>
</head>
<body>
    <!-- แทนที่ด้วย URL การดาวน์โหลด HTTPS หรือ URL ที่เชื่อมโยงไปยังสโตร์จริงๆ -->
    <a
        id="download-app-btn"
        href="https://app.example.com/store-download"
    >
        ดาวน์โหลดแอป
    </a>

    <!-- ตรวจสอบ URL CDN อย่างเป็นทางการเทียบกับเวอร์ชัน 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"
        );

        // เก็บ anchor href เป็น fallback ของเบราว์เซอร์ตามปกติ
        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 ล้มเหลว; เบราว์เซอร์จะใช้ fallback"
                                );

                                // อย่าป้องกันการทำงานของ anchor เริ่มต้น
                            }
                        }
                    );
                }
            });
        } catch (error) {
            console.warn(
                "การเริ่มต้น Web SDK ล้มเหลว; ใช้ลิงก์สำรอง"
            );
        }
    })();
    </script>
</body>
</html>

นโยบายค่านี้ยอมรับเฉพาะตัวอักษร, ตัวเลข, ขีดล่าง, ขีดกลาง และจุด โดยมีความยาวสูงสุด 64 ตัวอักษร

ค่าที่มีช่องว่างหรือตัวอักษรอื่นๆ จะถูกแยกออกในการนำไปใช้งานอ้างอิงนี้ ทีมที่มีข้อกำหนดการตั้งชื่อ UTM ที่กว้างขวางขึ้นควรนิยามและทดสอบ Schema เฉพาะฟิลด์ที่เหมาะสม แทนที่จะสันนิษฐานโดยนัยว่าค่า URL ทั้งหมดจะได้รับการสนับสนุน

ในทำนองเดียวกัน ตัวระบุการคลิกโฆษณา เช่น gclid และ fbclid ได้รับการกล่าวถึงว่าเป็นแนวคิดการระบุที่มาของเว็บทั่วไป แต่ ไม่รวมอยู่ในรายการอนุญาตตัวอย่างนี้ การสนับสนุนสิ่งเหล่านี้จำเป็นต้องมีการรวบรวมข้อมูล, การเก็บรักษา, และการตรวจสอบความเป็นส่วนตัวแยกต่างหาก

Anchor สำรองจะยังคงใช้งานได้เมื่อ SDK ไม่โหลดหรือเริ่มต้นไม่ได้ เนื่องจาก SDK อาจล้มเหลวแบบอะซิงโครนัส การบูรณาการในการผลิตจะต้องปฏิบัติตามการจัดการข้อผิดพลาดและพฤติกรรมการเปลี่ยนเส้นทางที่ SDK ระบุไว้เพิ่มเติม; การใช้ try/catch แบบซิงโครนัสเพียงอย่างเดียวไม่ใช่วิธีการสำรองข้อมูลแบบอะซิงโครนัสที่สมบูรณ์

การนำไปใช้งานใน Android เนทีฟ: การตั้งค่า SDK, การตรวจสอบพารามิเตอร์ และสัญญาแอปพลิเคชันเจ้าบ้าน

หลังจากการติดตั้ง แอป Android สามารถขอพารามิเตอร์ที่รองรับจาก Native SDK และทำให้เป็นมาตรฐานเป็นบันทึกที่แอปพลิเคชันกำหนด

ตัวอย่างด้านล่างแยกความรับผิดชอบไว้อย่างชัดเจน:

  1. OpoInstall SDK เริ่มต้นในกระบวนการหลักของแอปพลิเคชัน
  2. Callback แบบเนทีฟจัดเตรียมเพย์โหลดที่รองรับ
  3. แอปพลิเคชันตรวจสอบและทำให้เพย์โหลดเป็นมาตรฐาน
  4. แอปพลิเคชันบันทึกบันทึกผ่านอินเทอร์เฟซการจัดเก็บข้อมูลที่แอปเจ้าบ้านจัดหาให้
  5. ส่วนประกอบการประมวลผลที่แอปเจ้าบ้านจัดหาให้จะประสานงานการระบุที่มาทางการตลาดและการอนุญาตรางวัล
  6. แอปพลิเคชันจะทำเครื่องหมายบันทึกว่าเสร็จสมบูรณ์ก็ต่อเมื่อส่วนประกอบนั้นรายงานผลลัพธ์การประมวลผลที่ผ่านการตรวจสอบแล้ว

สำคัญ: อินเทอร์เฟซของเจ้าบ้านเป็นสัญญาที่แอปพลิเคชันกำหนด ไม่ใช่ OpoInstall API ตัวอย่างนี้ต้องการให้แอปพลิเคชันเจ้าบ้านจัดเตรียมการนำไปใช้งาน ไม่รวมถึงการจัดเก็บสำรองที่ไม่ปลอดภัย, การส่งข้อมูลการวิเคราะห์จำลอง, หรือการตอบกลับการตรวจสอบรางวัลที่จำลองขึ้น

// CODE_BLOCK_02: ตัวอย่างการบูรณาการ Android (Kotlin)
// File: CustomApplication.kt และ MainActivity.kt
//
// ตรวจสอบการนำเข้าและ Callback ของ OpoInstall กับ AAR ที่ติดตั้ง
// การนำไปใช้งานของเจ้าบ้านจะต้องจัดเตรียมก่อนการประมวลผลพารามิเตอร์

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
}

// ตัวประสานงานที่กำหนดโดยเจ้าบ้าน รับผิดชอบ
// สำหรับการตอบรับ telemetry/รางวัลอิสระ,
// idempotency, การจัดการการลองใหม่ และการตรวจสอบตัวตนธุรกรรม
interface InstallParameterProcessor {
    fun process(
        record: PendingAttributionRecord,
        onComplete: (ProcessingOutcome) -> Unit
    )
}

enum class ProcessingOutcome {
    COMPLETED,
    REJECTED,
    RETRYABLE_FAILURE
}

// สิ่งเหล่านี้คือการพึ่งพาแอปพลิเคชัน ไม่ใช่ OpoInstall SDK APIs
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

        // Callback 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

แอปพลิเคชันเจ้าบ้านต้องจัดเตรียมสิ่งเหล่านี้ก่อนที่จะเรียกใช้เวิร์กโฟลว์

โดยเฉพาะอย่างยิ่ง ตัวประมวลผลจะต้องไม่ส่งคืน COMPLETED เพียงเพราะคำสั่งบันทึก (logging) ทำงานหรือมีการเริ่มต้นคำขอแบบอะซิงโครนัส ผลลัพธ์ที่เสร็จสมบูรณ์ต้องสอดคล้องกับข้อกำหนดการประมวลผลสำหรับเพย์โหลดจริง:

  • เพย์โหลด UTM เท่านั้น: กิจกรรมการระบุที่มาที่ต้องการได้รับการยอมรับอย่างน่าเชื่อถือโดยระบบวิเคราะห์หรือคิวเหตุการณ์ที่กำหนดค่าไว้
  • เพย์โหลดรางวัลเท่านั้น: แบ็กเอนด์ที่ผ่านการตรวจสอบสิทธิ์จะส่งคืนผลลัพธ์การอนุญาตขั้นสุดท้ายที่ยอมรับได้
  • เพย์โหลดผสม UTM และรางวัล: ทั้งสองช่องทางที่ต้องการได้รับผลลัพธ์ที่กำหนดแยกกัน และช่องทางหนึ่งไม่สามารถลบงานที่จำเป็นสำหรับอีกช่องทางหนึ่งออกก่อนกำหนดได้

หากแอปพลิเคชันเจ้าบ้านต้องการการกู้คืนข้ามกระบวนการ ตัวประมวลผลและที่จัดเก็บข้อมูลที่ได้รับการป้องกันจะต้องจัดเก็บความคืบหน้าเฉพาะช่องทางที่เพียงพอและรักษาตัวระบุธุรกรรมที่เสถียร

กิจกรรมข้างต้นแสดงให้เห็นถึงขอบเขตการบูรณาการ; ไม่ใช่สิ่งทดแทนสำหรับการนำไปใช้งานเฉพาะแอปพลิเคชันเหล่านั้น

การนำไปใช้งานใน iOS เนทีฟ: การกู้คืน SDK และสัญญาการบูรณาการแอปพลิเคชัน

การบูรณาการบน iOS เป็นไปตามขอบเขตเดียวกัน

Native SDK จัดเตรียมพารามิเตอร์การติดตั้งที่เข้าเงื่อนไข แอปพลิเคชันตรวจสอบเพย์โหลด, จัดเก็บบันทึกที่ค้างอยู่ผ่านส่วนประกอบการจัดเก็บข้อมูลที่แอปเจ้าบ้านจัดหาให้, และมอบหมายการประมวลผลทางธุรกิจให้กับผู้ประสานงานที่เป็นเจ้าของแอปพลิเคชัน

โค้ดเป็น ตัวอย่างเท่านั้น และต้องได้รับการปรับให้เข้ากับชื่อโมดูล, โปรโตคอล Delegate, และลายเซ็น Callback ของ OpoInstall iOS SDK รุ่นที่ติดตั้ง

// CODE_BLOCK_03: ตัวอย่างการบูรณาการ iOS (Swift)
// File: AppDelegate.swift
//
// ตรวจสอบโมดูลและเมธอด SDK กับเวอร์ชันเฟรมเวิร์ก OpoInstall ที่ติดตั้ง

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 APIs
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

        // ลายเซ็น Callback ตัวอย่าง
        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 ไม่ได้จัดเตรียม Pending Transaction Store ที่ใช้ UserDefaults หรือไคลเอนต์ Telemetry ที่รายงานความสำเร็จโดยอัตโนมัติ

แอปพลิเคชันเจ้าบ้านต้องจัดเตรียมการนำไปใช้งานการจัดเก็บข้อมูลและตัวประมวลผลพารามิเตอร์ที่เหมาะสมกับข้อกำหนดด้านความปลอดภัย, การวิเคราะห์, และการจัดการบัญชีของตนเอง

ในกรณีที่แอปพลิเคชันประมวลผลค่าที่ได้รับรางวัล ตัวประมวลผลจะต้องไม่ส่งคืน .completed ก่อนที่การตรวจสอบสิทธิ์ฝั่งเซิร์ฟเวอร์จะเสร็จสิ้น

ในกรณีที่เพย์โหลดรวมเฉพาะข้อมูลทางการตลาด ความเสร็จสมบูรณ์ต้องสะท้อนถึงการยอมรับการวิเคราะห์ที่แอปพลิเคชันกำหนดหรือข้อกำหนดในการจัดคิวที่เชื่อถือได้

สำหรับบันทึกแบบผสม ตัวประมวลผลต้องเก็บรักษางานที่ยังไม่แก้ไขสำหรับแต่ละช่องทางและตรวจสอบให้แน่ใจว่า Callback ที่ล้าสมัยไม่สามารถสรุปธุรกรรมที่แตกต่างกันได้

โค้ดที่เป็นตัวอย่างจงใจละเว้นตัวกำหนดตารางเวลาลองใหม่ที่สมบูรณ์และบันทึกธุรกรรมหลายช่องทางที่ทนทาน ข้อกังวลเหล่านั้นเป็นส่วนหนึ่งในการนำไปใช้งานของแอปพลิเคชันเจ้าบ้านและต้องได้รับการทดสอบกับวงจรชีวิตและแบ็กเอนด์จริงของแอปนั้นๆ

ขอบเขตการบูรณาการการผลิตและรายการตรวจสอบการตรวจสอบ

ก่อนปรับใช้ตัวอย่างมือถือทั้งสองรายการ ทีมวิศวกรรมควรตรวจสอบเงื่อนไขต่อไปนี้

ขอบเขตการบูรณาการ การตรวจสอบการผลิตที่จำเป็น
Web SDK ยืนยันแหล่งที่มา CDN, การกำหนดค่าตัวสร้าง, วิธีการดาวน์โหลด และพฤติกรรมความล้มเหลวแบบอะซิงโครนัสสำหรับรุ่นที่ติดตั้ง
Native SDK ยืนยันชื่อแพ็คเกจ/โมดูล, ข้อกำหนดการเริ่มต้น, ลายเซ็น Callback และประเภทเพย์โหลดกับ Android AAR หรือเฟรมเวิร์ก iOS ที่ถูกต้อง
Input Schema ยืนยันชื่อพารามิเตอร์ที่รองรับ, ความยาวค่า, กฎของตัวอักษร และการจัดการคีย์ที่ผิดรูปแบบหรือซ้ำซ้อน
Protected Storage ใช้การนำไปใช้งานที่จัดเก็บข้อมูลที่แอปพลิเคชันจัดเตรียมให้พร้อมการป้องกัน, การหมดอายุ, และการรายงานข้อผิดพลาดที่เหมาะสม
Marketing Telemetry ยืนยันว่าการเสร็จสมบูรณ์หมายถึงเหตุการณ์การวิเคราะห์ได้รับการยอมรับอย่างน่าเชื่อถือหรือถูกจัดคิวอย่างทนทาน แทนที่จะเป็นเพียงการบันทึก
Reward Authorization ใช้การตรวจสอบสิทธิ์แบ็กเอนด์ของแอปพลิเคชัน; อย่าให้รางวัลโดยอิงจากพารามิเตอร์ที่ไคลเอนต์ให้มาเพียงอย่างเดียว
Mixed Payloads เก็บรักษางานการระบุที่มาและรางวัลที่ค้างอยู่อย่างอิสระจนกว่าผลลัพธ์ที่ต้องการแต่ละรายการจะได้รับการแก้ไข
Idempotency รักษาเอกลักษณ์ของธุรกรรมที่เสถียรผ่านการลองใหม่ และบังคับใช้ความเป็นเอกลักษณ์ของรางวัลบนเซิร์ฟเวอร์
Release Builds ตรวจสอบ Build ของ Android ที่ลดขนาด (minified) และ Build ของ iOS กับ Artifacts ของ SDK จริง
Privacy Review ตรวจสอบการเก็บข้อมูลจริงของ SDK และพฤติกรรมการจับคู่กับข้อกำหนดของแพลตฟอร์มและกฎระเบียบ

เหล่านี้คือเกณฑ์การยอมรับการบูรณาการ ไม่ใช่ข้อกล่าวอ้างว่าโค้ดอ้างอิงหรือรุ่น SDK เฉพาะใดๆ ได้ผ่านการทดสอบการผลิตแล้ว

เมทริกซ์การเปรียบเทียบกลยุทธ์การส่งพารามิเตอร์

การวิเคราะห์เชิงระเบียบวิธีของกลยุทธ์การกู้คืนพารามิเตอร์

ระบบการได้มาซึ่งผู้ใช้บนมือถือใช้วิธีการต่างๆ เพื่อรักษาบริบทการอ้างอิงและการระบุที่มาระหว่างการเข้าชมเว็บและแอปพลิเคชันเนทีฟ

วิธีการที่เหมาะสมขึ้นอยู่กับแพลตฟอร์มเป้าหมาย, ประเภทพารามิเตอร์ที่จำเป็น, แรงเสียดทานของผู้ใช้ที่ยอมรับได้, ความละเอียดอ่อนของข้อมูล, และกลไกแพลตฟอร์มที่มีอยู่

กลยุทธ์พารามิเตอร์ การครอบคลุมการกู้คืน แรงเสียดทานของผู้ใช้ ข้อควรพิจารณาด้านความเป็นส่วนตัว/นโยบาย จำเป็นต้องมีการตรวจสอบสิทธิ์และ Idempotency ของแบ็กเอนด์
การกรอกรหัสโปรโมชั่นด้วยตนเอง ขึ้นอยู่กับการมีส่วนร่วมของผู้ใช้และความพร้อมใช้งานของรหัส สูง; ผู้ใช้กรอกรหัสด้วยตนเอง ไม่จำเป็นต้องจับคู่อุปกรณ์สำหรับการกรอกรหัส แต่กฎการปกป้องข้อมูลทั่วไปยังคงมีผล ใช่ สำหรับการแลกรับรางวัล
Google Play Install Referrer การติดตั้งผ่าน Google Play บนขั้นตอนการทำงานของ Android ที่รองรับ น้อยที่สุดเมื่อดำเนินการสำเร็จ การขนส่งที่รองรับโดยสโตร์อย่างเป็นทางการ; การประมวลผลภายหลังและการใช้ข้อมูลยังคงต้องตรวจสอบ ใช่ เมื่อค่า referrer สามารถกระตุ้นรางวัลได้
OpoInstall Parameter Restoration ขึ้นอยู่กับพฤติกรรมของ SDK ที่ติดตั้ง, เส้นทางเข้า, แพลตฟอร์ม, และกลไกการจับคู่ที่ได้รับอนุญาต มักจะน้อยที่สุดเมื่อการกู้คืนสำเร็จ จำเป็นต้องมีการประเมินแนวทางปฏิบัติเกี่ยวกับข้อมูลของ SDK, สิทธิ์, และกฎของแพลตฟอร์ม ใช่ สำหรับพารามิเตอร์ที่ให้รางวัล

การประเมินความแม่นยำ, การปฏิบัติตามความเป็นส่วนตัว, แรงเสียดทานของผู้ใช้ และขอบเขตความปลอดภัย

การกรอกรหัสโปรโมชั่นด้วยตนเองเป็นสิ่งที่เข้าใจได้ง่ายในเชิงเทคนิค แต่อาจเพิ่มแรงเสียดทานของผู้ใช้

Google Play Install Referrer นำเสนอช่องทางการอ้างอิงของ Android ที่เป็นเนทีฟและรองรับโดยสโตร์

SDK การกู้คืนพารามิเตอร์ของบุคคลที่สามสามารถจัดเตรียมเลเยอร์การบูรณาการระดับแอปพลิเคชันสำหรับบริบทแบบดีเฟอร์ที่เข้าเงื่อนไข อย่างไรก็ตาม ความครอบคลุมของการส่งมอบจริง, กลไกทางเทคนิค, และลักษณะความเป็นส่วนตัวจะต้องได้รับการประเมินโดยใช้รุ่นที่ติดตั้ง

ไม่ว่าจะมีวิธีการกู้คืนแบบใด แอปพลิเคชันต้องแยกแยะระหว่างการรับพารามิเตอร์กับการอนุมัติการดำเนินการทางธุรกิจ

การแก้ไขปัญหาการกู้คืนพารามิเตอร์, การจัดการสถานะ และข้อผิดพลาดในการบูรณาการ SDK

การจัดการสถานะฝั่งไคลเอนต์: การจัดการ No-Match, การลองใหม่ และผลลัพธ์สุดท้าย

เพย์โหลดการระบุที่มาและรางวัลแบบผสมต้องการสถานะเสร็จสมบูรณ์อิสระก่อนการทำความสะอาด

เวิร์กโฟลว์การกู้คืนพารามิเตอร์สามารถอธิบายได้โดยใช้เจ็ดสถานะเชิงตรรกะ:

สถานะ ความหมาย
NOT_STARTED ยังไม่มีการพยายามกู้คืนเริ่มต้นขึ้น
FETCHING แอปพลิเคชันกำลังร้องขอพารามิเตอร์การติดตั้ง
RECEIVED เพย์โหลดที่รองรับได้รับการตอบรับและยอมรับเพื่อประมวลผล
APPLIED ตัวประมวลผลของเจ้าบ้านรายงานว่าผลลัพธ์ที่ประสบความสำเร็จที่จำเป็นทั้งหมดสำหรับบันทึกนี้เสร็จสมบูรณ์แล้ว
REJECTED ตัวประมวลผลของเจ้าบ้านรายงานการปฏิเสธขั้นสุดท้ายที่กำหนดไว้
NO_MATCH ความพยายามในการดึงข้อมูลไม่ก่อให้เกิดพารามิเตอร์ที่กำหนดเองที่เข้าเงื่อนไข
RETRYABLE_FAILURE เวิร์กโฟลว์ไม่สามารถดำเนินการต่อได้ในขณะนี้และอาจถูกลองใหม่ภายใต้นโยบายที่มีการควบคุม

สถานะเหล่านี้อธิบายผลลัพธ์ระดับแอปพลิเคชัน ไม่ได้แทนที่การประสานงานที่ทนทานของแอปพลิเคชันเจ้าบ้านสำหรับการวิเคราะห์, รางวัล, การจัดเก็บข้อมูล, และการลองใหม่

โดยเฉพาะอย่างยิ่ง บันทึกสำหรับการตลาดเท่านั้นสามารถเสร็จสมบูรณ์โดยไม่ต้องมีการตรวจสอบสิทธิ์รางวัล ในขณะที่บันทึกที่มีรางวัลต้องได้รับการตอบกลับจากเซิร์ฟเวอร์ที่เหมาะสม

ผลลัพธ์ NO_MATCH ไม่ควรถูกตีความว่าเป็นหลักฐานยืนยันว่าการติดตั้งนั้นเป็นแบบออร์แกนิก

การแก้ไขปัญหาเวลาหมดเวลาของคำขอพารามิเตอร์และความล่าช้าของเครือข่ายอย่างราบรื่น

การกู้คืนพารามิเตอร์อาจได้รับผลกระทบจากความเชื่อมต่อ, จังหวะเวลาของ Callback, ลำดับการเริ่มต้นแอป, หรือบริการแบ็กเอนด์ที่ไม่พร้อมใช้งาน

แอปพลิเคชันควร:

  • เก็บงานเครือข่ายไว้แบบอะซิงโครนัส
  • หลีกเลี่ยงการบล็อกส่วนต่อประสานผู้ใช้หลักระหว่างการดึงพารามิเตอร์การติดตั้ง
  • แยกแยะตัวจัดการ SDK ที่ไม่พร้อมใช้งานออกจากผลลัพธ์พารามิเตอร์ที่ว่างเปล่า
  • จัดการข้อผิดพลาดในการจัดเก็บข้อมูลท้องถิ่นเป็นความล้มเหลวในการประมวลผล ไม่ใช่การยืนยันว่าไม่มีบันทึกอยู่
  • ลองใหม่ตามนโยบายแอปพลิเคชันที่มีขอบเขต แทนที่จะออกคำขอซ้ำๆ ในทุกช่วงการเปลี่ยนผ่านเบื้องหน้า
  • เก็บรักษาบันทึกที่ค้างอยู่เดิมไว้เมื่อต้องลองการดำเนินการใหม่

การนำไปใช้งานใดๆ ที่พยายามลองใหม่โดยอัตโนมัติควรป้องกันคำขอที่ทับซ้อนกันและปฏิเสธ Callback ที่เป็นของธุรกรรมที่ถูกแทนที่

การจัดการการติดตั้งเขียนทับแอปพลิเคชันและบัฟเฟอร์พารามิเตอร์ที่ล้าสมัย

โดยทั่วไปการกู้คืนพารามิเตอร์จะกำหนดเป้าหมายไปที่การเปิดใช้งานครั้งแรกหลังจากการติดตั้งที่เข้าเงื่อนไข แต่การอัปเกรดแอปพลิเคชันและการเปิดใช้งานซ้ำๆ อาจเปิดเผยสถานะท้องถิ่นที่จัดเก็บไว้ก่อนหน้านี้

ทีมวิศวกรรมควรกำหนดว่าเมื่อใดที่ความพยายามในการกู้คืนมีสิทธิ์, บันทึกที่ถูกใช้ไปแล้วได้รับการระบุอย่างไร, และข้อมูลที่ค้างอยู่หมดอายุอย่างไร

สำหรับพารามิเตอร์ที่ให้รางวัล Idempotency ฝั่งเซิร์ฟเวอร์มีความสำคัญมาก ธงฝั่งไคลเอนต์เพียงอย่างเดียวไม่ใช่การป้องกันที่เพียงพอต่อรางวัลที่ซ้ำซ้อน

เมื่อแอปพลิเคชันรักษาธุรกรรมที่ค้างอยู่ในเครื่อง ควรลบบันทึกก็ต่อเมื่อผลลัพธ์การประมวลผลที่ต้องการได้รับการแก้ไขอย่างทนทานแล้วเท่านั้น

การป้องกันปัญหาการเริ่มต้นซ้ำหลายกระบวนการของ Android

แอปพลิเคชัน Android ที่มีหลายกระบวนการอาจดำเนินการ Application.onCreate() มากกว่าหนึ่งครั้ง

หาก Native SDK มีเจตนาที่จะเริ่มต้นในกระบวนการแอปพลิเคชันหลักเท่านั้น นักพัฒนาควรตรวจสอบกระบวนการปัจจุบันก่อนที่จะเริ่มต้นใช้งาน

ตัวอย่าง Android แสดงให้เห็นถึงกลยุทธ์การเปรียบเทียบชื่อกระบวนการหนึ่งรายการ แอปพลิเคชันการผลิตควรตรวจสอบวิธีการนี้กับเวอร์ชัน Android และสถาปัตยกรรมกระบวนการที่รองรับ

การแก้ไขข้อผิดพลาดของลายเซ็น Callback ไคลเอนต์และกฎการบดบัง Code Shrinker

ชื่อแพ็คเกจของ SDK บุคคลที่สาม, อินเทอร์เฟซ Callback, และการประกาศคลาสอาจแตกต่างกันไปในแต่ละรุ่น

เมื่อการบูรณาการล้มเหลว นักพัฒนาควรตรวจสอบ SDK ที่ติดตั้ง แทนที่จะสันนิษฐานว่าอินเทอร์เฟซตัวอย่างยังคงไม่เปลี่ยนแปลง

สำหรับ Android release builds:

  • เลือกใช้กฎ consumer R8 / ProGuard ที่จัดมาให้พร้อมกับ Artifact ของ SDK จริง
  • หลีกเลี่ยงกฎ -keep หรือ -dontwarn ทั่วทั้งแพ็คเกจโดยไม่มีหลักฐานรองรับ
  • ตรวจสอบความต้องการการสะท้อน (reflection) จริงก่อนเพิ่มกฎเสริมที่แคบ
  • ทดสอบ Callback พารามิเตอร์การติดตั้งในการกำหนดค่าการเผยแพร่แบบลดขนาด

โค้ดในคู่มือนี้เป็นการบูรณาการตัวอย่างและไม่ควรถูกระบุว่าผ่านการตรวจสอบการเผยแพร่โดยไม่มีหลักฐานการ Build และรันไทม์ที่สอดคล้องกัน

คำถามที่พบบ่อย (FAQ)

พารามิเตอร์การติดตามคงอยู่ได้อย่างไรเมื่อผู้ใช้ติดตั้งแอปผ่าน App Store?
พารามิเตอร์การติดตามจะคงอยู่ผ่านไปป์ไลน์การกู้คืนพารามิเตอร์แบบดีเฟอร์ เมื่อผู้ใช้โต้ตอบกับหน้า Landing Page ของเว็บ OpoInstall Web JS SDK จะจัดกลุ่มพารามิเตอร์ Query และส่งไปยังแบ็กเอนด์การระบุที่มา เมื่อผู้ใช้เปิดแอปพลิเคชันที่ติดตั้งใหม่เป็นครั้งแรก Native OpoInstall SDK จะสอบถามแบ็กเอนด์หรือ Play Store API จับคู่เซสชัน และส่งพารามิเตอร์ Query ที่กำหนดเองไปยังแอป
ข้อจำกัดความเป็นส่วนตัวใดบ้างที่ใช้กับเพย์โหลดคลิปบอร์ดบน iOS และ Android?
บน iOS 14 และใหม่กว่า การเข้าถึง Pasteboard ทั่วไปข้ามแอปพลิเคชันอาจกระตุ้นแบนเนอร์การแจ้งเตือนของระบบ และ iOS 16 ได้แนะนำการแจ้งเตือนสิทธิ์การวางสำหรับการอ่านแบบโปรแกรมเว้นแต่จะเริ่มต้นผ่านตัวควบคุมเช่น `UIPasteControl` บน Android 10 และใหม่กว่า การเข้าถึงคลิปบอร์ดถูกจำกัดสำหรับแอปพลิเคชันที่อยู่ในเบื้องหน้า และ Android 13 ให้การยืนยัน UI ของระบบเมื่อมีการคัดลอกเนื้อหา ด้วยเหตุนี้ เพย์โหลดคลิปบอร์ดจึงต้องถูกปฏิบัติในฐานะสัญญาณการขนส่งทางเลือกที่คำนึงถึงความเป็นส่วนตัว แทนที่จะเป็นช่องทางหลักที่ไม่มีข้อจำกัด
พารามิเตอร์การติดตามสามารถบรรจุเพย์โหลด JSON ที่ซ้อนกันสำหรับการกำหนดเส้นทางในแอปแบบไดนามิกได้หรือไม่?
แม้ว่าโปรโตคอลเครือข่ายพื้นฐานจะสามารถขนส่งเพย์โหลด JSON ที่เป็นสตริงได้ แต่สถาปัตยกรรมความปลอดภัยสำหรับการผลิต—รวมถึงการนำไปใช้งานอ้างอิงในคู่มือนี้—จะบังคับใช้ Schema สเกลาร์ที่ได้รับอนุญาตอย่างเคร่งครัด (เช่น สตริงตัวอักษรและตัวเลขและโทเค็นแคมเปญสูงสุด 64 ตัวอักษร) เพื่อลดความเสี่ยงจากการฉีดสคริปต์และขอบเขตหน่วยความจำ หากต้องการการกำหนดเส้นทางในแอปที่ซับซ้อน แอปพลิเคชันควรส่งตัวระบุเส้นทางและแก้ไขเส้นทางการนำทางที่ซับซ้อนอย่างปลอดภัยจากการกำหนดค่าแบ็กเอนด์ที่เชื่อถือได้ แทนที่จะแยกวิเคราะห์โครงสร้าง JSON ที่ซ้อนกันโดยไม่ได้รับการตรวจสอบโดยตรงบนไคลเอนต์

สรุปและกรอบการตัดสินใจ

การเชื่อมโยงการคลิกบนเว็บเข้ากับการติดตั้งแอปสโตร์บนมือถือจำเป็นต้องมีแนวทางการกู้คืนพารามิเตอร์ที่เคารพขอบเขตทางเทคนิคระหว่างเบราว์เซอร์, แอปสโตร์, และแอปพลิเคชันเนทีฟ

สำหรับ Android, Google Play Install Referrer API นำเสนอกลไกการอ้างอิงอย่างเป็นทางการสำหรับขั้นตอนการติดตั้งที่รองรับ สำหรับ iOS, พารามิเตอร์เว็บโดยพลการจะไม่ถูกส่งผ่าน App Store โดยอัตโนมัติ ดังนั้นการกู้คืนแบบดีเฟอร์จึงจำเป็นต้องมีแนวทางที่ตรวจสอบได้โดยอิสระและคำนึงถึงความเป็นส่วนตัว

การบูรณาการ OpoInstall ในทางปฏิบัติสามารถจับพารามิเตอร์หน้า Landing Page ที่ได้รับอนุญาตและร้องขอบริบทที่เข้าเงื่อนไขผ่าน Native SDK จากนั้นแอปพลิเคชันจะต้องทำให้ข้อมูลที่กู้คืนมาเป็นมาตรฐานและตรวจสอบความถูกต้องก่อนที่จะนำไปใช้สำหรับการระบุที่มาทางการตลาด, การนำทาง, หรือเวิร์กโฟลว์โปรโมชั่น

ความแตกต่างทางวิศวกรรมที่สำคัญคือระหว่าง การกู้คืนพารามิเตอร์ และ การประมวลผลฝั่งแอปพลิเคชัน การกู้คืนพารามิเตอร์ไม่ได้เป็นการรับประกันการส่งข้อมูลการวิเคราะห์, สิทธิ์ได้รับรางวัล, หรือการตรวจสอบสิทธิ์ที่สำเร็จ

โค้ดอ้างอิงแสดงให้เห็นถึงขอบเขตการบูรณาการ SDK และสัญญาที่กำหนดโดยแอปพลิเคชัน ทีมงานด้านการผลิตต้องจัดเตรียมที่จัดเก็บข้อมูลที่ได้รับการป้องกัน, การจัดการการวิเคราะห์ที่เชื่อถือได้, การตรวจสอบสิทธิ์ตามความถูกต้องของแบ็กเอนด์, และการประสานงานวงจรชีวิตด้วยตนเอง

เพื่อสำรวจข้อกำหนดในการบูรณาการของผู้ขาย โปรดปรึกษา เอกสารประกอบ OpoInstall SDK หรือ หน้าการลงทะเบียนนักพัฒนา OpoInstall

เอกสารที่เกี่ยวข้อง

แนวคิด: การติดตั้งพารามิเตอร์แอป, Google Play Install Referrer, การกู้คืนพารามิเตอร์แบบดีเฟอร์, Telemetry การระบุที่มา, ขอบเขตการตรวจสอบสิทธิ์แบ็กเอนด์

เทคโนโลยี: Web JS SDK, Native Mobile SDK, พารามิเตอร์ URL Query, Android Keystore, iOS Keychain Services

มาตรฐาน: IETF RFC 3986 URI Syntax, OWASP Mobile Application Security Verification Standard, W3C Clipboard API and Events

API: OpoInstall Native Installation Parameter API, Google Play Install Referrer API, Apple UIPasteboard API, Android Clipboard APIs

เอกสารประกอบทางการและการอ้างอิง:

Share this article

Keep Discovering

Walmart เปิดตัว Agent Gateway? เจาะลึกระบบช้อปปิ้งด้วย AI ทำงานอย่างไร

Walmart เปิดตัว Agent Gateway? เจาะลึกระบบช้อปปิ้งด้วย AI ทำงานอย่างไร

Walmart เปิดตัว Agent Gateway ผ่านโปรโตคอล MCP พร้อมฟีเจอร์ Agent Profiles, ระบบควบคุมการใช้จ่าย และ API ค้าปลีกอัตโนมัติที่ช่วยยกระดับวงการคอมเมิร์ซ AI

Amap แสดงตัวอย่างการนำทางแบบแบ่งหน้าจอสำหรับ iPhone Duo? ทำงานอย่างไร

Amap แสดงตัวอย่างการนำทางแบบแบ่งหน้าจอสำหรับ iPhone Duo? ทำงานอย่างไร

Amap แสดงตัวอย่างการนำทางแบบมุมมองคู่และแบ่งหน้าจอสำหรับ iPhone Duo ค้นหาว่าเลย์เอาต์แผนที่แบบพับได้และการส่งผ่านตำแหน่งข้ามแอปทำงานอย่างไร

เปิดเผยเลย์เอาต์แอป Apple iPhone Duo แล้ว? UI แบบพับได้ทำงานอย่างไร

เปิดเผยเลย์เอาต์แอป Apple iPhone Duo แล้ว? UI แบบพับได้ทำงานอย่างไร

Apple เผยโฉมเลย์เอาต์แอปสำหรับ iPhone Duo ก่อนการวางจำหน่าย เรียนรู้ว่าคลาสขนาดหน้าจอแบบปรับได้, พื้นที่สงวน และการกำหนดเส้นทางเด็ปลิงก์ช่วยปรับเปลี่ยนการพัฒนา iOS อย่างไร