การแมป Conversion Value ของ SKAdNetwork: การทำงานอัตโนมัติสำหรับสคีมา SKAN แบบไดนามิก

opoinstall
2026-08-25
5 min read

MMP แมปสคีมา SKAdNetwork โดยอัตโนมัติได้อย่างไร? พาร์ทเนอร์การวัดผลโมบายล์ (MMP) หรือแบ็กอัปการระบุแหล่งที่มา จะทำงานอัตโนมัติในการแมปสคีมา SKAdNetwork โดยแปลงอีเวนต์ในแอปและระดับรายได้ให้เป็นสคีมาการกำหนดค่า JSON แบบไดนามิกที่มีการจัดเวอร์ชันบนคอนโซลส่วนกลาง SDK มือถือจะดึงข้อมูลการกำหนดค่านี้ตอนเปิดแอปและประเมินกฎ conversion ในเครื่องขณะรันไทม์ ซึ่งช่วยให้การเปลี่ยนแปลงกฎ conversion ที่รองรับมีผลบังคับใช้ได้ทันทีโดยไม่ต้องออกรุ่นไบนารีแอปใหม่

สคีมา Conversion Value ของ SKAdNetwork คือชุดกฎระดับผู้จำหน่ายหรือระดับแอปพลิเคชัน ที่แมปพฤติกรรมผู้ใช้ในแอป เช่น ธุรกรรมรายได้, ขั้นตอนการเริ่มต้นใช้งาน (onboarding), หรือการมีส่วนร่วมกับฟีเจอร์ เข้ากับค่าละเอียด 6 บิตของ Apple (0 ถึง 63) และค่าหยาบ 3 ระดับ (low, medium, high) สถาปัตยกรรมการแมปแบบไดนามิกจะแจกจ่ายไฟล์กำหนดค่าที่มีการจัดเวอร์ชันจากแบ็กอัปบนคลาวด์ไปยัง SDK ของไคลเอนต์ ซึ่งช่วยลดความจำเป็นในการฮาร์ดโค้ดลอจิก conversion ไว้ภายในไบนารีแอปพลิเคชัน iOS ที่คอมไพล์แล้ว

คำศัพท์ คำจำกัดความ
SKAdNetwork เฟรมเวิร์กในระดับแพลตฟอร์มของ Apple สำหรับการระบุแหล่งที่มาแคมเปญการตลาดที่รักษาความเป็นส่วนตัว
Conversion Value Schema การกำหนดค่าที่กำหนดโดยผู้จำหน่ายหรือแอป เพื่อแมปเหตุการณ์สำคัญในแอปกับค่าแบบละเอียดและแบบหยาบ
Dynamic Schema Mapping การแจกจ่ายและการประเมินกฎ conversion แบบอัตโนมัติขณะรันไทม์ผ่าน SDK
Window Locking พารามิเตอร์ API (lockWindow: true) ที่ทำให้หน้าต่าง conversion ที่ใช้งานอยู่สิ้นสุดเร็วกว่ากำหนด

สถาปัตยกรรมการแมป Conversion Value ของ SKAdNetwork แบบอัตโนมัติ

การแยกระหว่างเลเยอร์แพลตฟอร์มของ Apple และเลเยอร์สคีมาของผู้จำหน่าย

ในการออกแบบเอ็นจิ้น Conversion Value ที่แข็งแกร่ง ทีมวิศวกรต้องแยกระหว่างกฎเฟรมเวิร์กดั้งเดิมของ Apple ออกจากนามธรรมของสคีมาในระดับผู้จำหน่าย:

  • เลเยอร์แพลตฟอร์มของ Apple: ควบคุมพริมิติฟหลักของระบบปฏิบัติการ รวมถึงหน้าต่าง conversion สามช่วงตามลำดับ (วันที่ 0–2, วันที่ 3–7, และวันที่ 8–35 หลังจากเปิดใช้งานครั้งแรก), ค่าละเอียด 6 บิต (0–63), ค่าหยาบ (low, medium, high), ระดับข้อมูล postback, และ API SKAdNetwork.updatePostbackConversionValue
  • เลเยอร์สคีมาของผู้จำหน่าย: ครอบคลุมถึงกฎธุรกิจที่กำหนดโดยแอปพลิเคชัน เช่น การจัดกลุ่มรายได้, ความก้าวหน้าของฟันเนลการเริ่มต้นใช้งาน, การจัดสรรบิตต์ฟลาส, การซิงโครไนซ์ JSON จากระยะไกล, และการประเมินกฎฝั่งไคลเอนต์
┌────────────────────────────────────────┐
│                           Vendor Schema Layer                                  │
│  [MMP / Analytics Console] ──► [Publishes Versioned JSON Configuration]     │
│                                              │                                │
│  [Client Mobile SDK]       ──► [Evaluates In-App Events Locally in Memory]  │
└──────────────────────────────────────┬─┘
                                       │ (Calculates Fine, Coarse, & Lock)
                                       ▼
┌────────────────────────────────────────┐
│                           Apple Platform Layer                                 │
│  [StoreKit Framework]      ──► [SKAdNetwork.updatePostbackConversionValue]  │
│  [Operating System]        ──► [Manages Conversion Windows & Timers]        │
│  [System]                  ──► [Prepares and Sends Signed Postback]         │
└────────────────────────────────────────┘

การแมปสคีมา SKAN แบบไดนามิกจากคอนโซล MMP ไปยัง StoreKit

ข้อเสียของการฮาร์ดโค้ดลอจิก Conversion

การฮาร์ดโค้ดลอจิก conversion ไว้โดยตรงภายในเป้าหมายแอปพลิเคชัน iOS จะสร้างข้อจำกัดในการดำเนินงานที่สำคัญ:

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

ไปพไลน์การส่งมอบการกำหนดค่าแบบไดนามิก

สถาปัตยกรรมการแมปแบบอัตโนมัติช่วยแยกตรรกะ conversion ออกจากไบนารีที่คอมไพล์แล้วผ่านไปพไลน์หลายขั้นตอน:

การกำหนดค่าคอนโซล: นักการตลาดและนักวิเคราะห์กำหนดค่าน้ำหนักอีเวนต์, ระดับสกุลเงิน, และกฎการล็อกหน้าต่างบนแดชบอร์ดส่วนกลาง
  • การจัดเวอร์ชันและการตรึงสคีมา (Schema Versioning and Pinning): แบ็กอัปจะเผยแพร่เพย์โหลดการกำหนดค่า JSON ที่มีการจัดเวอร์ชัน เพื่อป้องกันความเบี่ยงเบนทางความหมายในช่วงวงจรชีวิต conversion 35 วันของผู้ใช้ การใช้งานของผู้จำหน่ายที่มีประสิทธิภาพจะทำการตรึงการกำหนดค่าสคีมาที่ใช้งานซึ่งสร้างขึ้นในช่วงหน้าต่าง conversion เริ่มต้น เพื่อให้มั่นใจว่ากฎการแมปที่แน่นอนจะยังคงใช้งานได้แม้ว่าจะมีการรีสตาร์ทแอปก็ตาม
  • การรับข้อมูลและการแคชของไคลเอนต์: SDK มือถือจะดาวน์โหลดสคีมาที่ใช้งานอยู่เมื่อเริ่มต้นแอป และแคชทั้งเพย์โหลดการกำหนดค่าและเมทาดาต้าของเวอร์ชันไว้ในหน่วยความจำถาวรภายในเครื่อง
  • การประเมินกฎในเครื่อง: เมื่อเกิดอีเวนต์ขึ้นในแอป SDK จะประเมินอีเวนต์เหล่านั้นเทียบกับชุดกฎที่แคชไว้ในเครื่อง โดยไม่ต้องเพิ่มคำขอการกำหนดค่าระยะไกลแบบซิงโครนัสลงในเส้นทางการทำงานของอีเวนต์
  • ลอจิก SKAN แบบฮาร์ดโค้ดเทียบกับการกำหนดค่าสคีมาแบบไดนามิก

    ดูเพิ่มเติม: SKAdNetwork ──> สถาปัตยกรรมการระบุแหล่งที่มาของมือถือ

    การออกแบบสคีมา Conversion Value แบบไดนามิกข้ามหน้าต่าง SKAN 4.0

    การแบ่งส่วนสคีมาแบบหลายหน้าต่าง (Multi-Window Schema Partitioning)

    SKAdNetwork 4.0 จัดโครงสร้างการวัดผล conversion ออกเป็นสามหน้าต่างตามลำดับ ซึ่งยึดโยงกับการเปิดใช้งานแอปครั้งแรก:

    • หน้าต่างที่ 1 (วันที่ 0–2): 48 ชั่วโมงแรกหลังจากเปิดใช้งานครั้งแรก
    • หน้าต่างที่ 2 (วันที่ 3–7): ชั่วโมงที่ 48 ถึง 168 หลังจากเปิดใช้งานครั้งแรก
    • หน้าต่างที่ 3 (วันที่ 8–35): ชั่วโมงที่ 168 ถึง 840 หลังจากเปิดใช้งานครั้งแรก

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

    หน้าต่างที่ 1 (วันที่ 0–2): การจัดโครงสร้างค่าแบบละเอียดและแบบหยาบ

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

    • การแมปแบบละเอียด (0–63): กฎความละเอียดสูงที่บันทึกระดับการสร้างรายได้เริ่มต้น, ขั้นตอนการเริ่มต้นใช้งาน, หรือคะแนนการมีส่วนร่วมแบบคอมโพสิต
    • การแมปแบบหยาบ (low, medium, high): สถานะสำรองความละเอียดต่ำที่จะเปิดเผยเมื่อระดับข้อมูล postback ที่กำหนดไม่อนุญาตให้มีการรายงานแบบละเอียด

    หน้าต่างที่ 2 (วันที่ 3–7) และ 3 (Day 8–35): การติดตามวงจรชีวิตแบบหยาบ

    Postback ครั้งที่สองและสามจะไม่เปิดเผยค่า conversion แบบละเอียด แต่สำหรับระดับข้อมูลที่มีสิทธิ์ จะเปิดเผยเฉพาะค่าแบบหยาบเท่านั้น

    สคีมาสำหรับหน้าต่างที่ 2 และ 3 จะมุ่งเน้นไปที่การรักษาผู้ใช้ระยะยาวและเหตุการณ์สำคัญในการสร้างรายได้:

    • การแมปแบบหยาบในหน้าต่างที่ 2: ประเมินการรักษาผู้ใช้ช่วงกลางฟันเนล (เช่น low = ใช้งานในวันที่ 3–7; medium = ทำครบ 3 เซสชัน; high = ซื้อซ้ำหรือทดลองใช้งานเปลี่ยนเป็นแบบชำระเงินแล้ว)
    • การแมปแบบหยาบในหน้าต่างที่ 3: ประเมินการรักษาผู้ใช้ระยะยาวและการต่ออายุการสมัครสมาชิก (เช่น low = ยังใช้งานอยู่ในวันที่ 8–35; medium = ถึงเป้าหมายเลเวล; high = สมาชิกแบบชำระเงินที่ยังใช้งานอยู่)

    นักพัฒนาที่กำหนดค่าสคีมา conversion สามารถศึกษา เอกสารประกอบการแมป conversion ของ SKAN เพื่อดูแนวทางทางเทคนิคเกี่ยวกับโครงสร้างกฎแบบหลายหน้าต่าง

    การแมปค่าแบบละเอียดและแบบหยาบในหน้าต่าง conversion ของ SKAN 4


    โมเดลการเข้ารหัสที่กำหนดโดยผู้จำหน่าย: การจัดกลุ่มรายได้, ฟันเนล, และลอจิกบิตส์

    โมเดลการเข้ารหัสเหล่านี้แสดงถึงรูปแบบการออกแบบในระดับผู้จำหน่ายและระดับแอปพลิเคชัน มากกว่าประเภทสคีมาที่ Apple กำหนด

    สคีมาที่อิงตามรายได้

    สคีมาการสร้างรายได้จะจัดสรรค่าแบบละเอียดที่มีอยู่ให้ครอบคลุมยอดซื้อสะสม:

    • การจัดกลุ่มแบบเส้นตรง (Linear Bucketing): แบ่งช่วงรายได้ออกเป็นช่วงเท่าๆ กัน (เช่น 64 บักเก็ต เพิ่มขึ้นทีละ $1.50 สูงสุดถึง $96.00) เหมาะสำหรับแอปพลิเคชันที่มีขนาดธุรกรรมที่คาดการณ์ได้
    • การจัดกลุ่มแบบลอการิทึม (Logarithmic Bucketing): จัดสรรบักเก็ตที่มีความละเอียดสูงให้กับรายการซื้อที่มีต้นทุนต่ำ ขณะเดียวกันก็ขยายช่วงบักเก็ตสำหรับการทำธุรกรรมที่มีมูลค่าสูง (เช่น ค่า 1–20 ครอบคลุม $0.99–$19.99; ค่า 21–50 ครอบคลุม $20.00–$100.00; ค่า 51–63 ครอบคลุม $100.00–$1000.00+)
    • การจัดกลุ่มตามเปอร์เซ็นต์ไทล์ (Percentile-Based Bucketing): แมปการกระจายการซื้อของผู้ใช้ในอดีตเข้ากับกลุ่ม cohorts ตามเส้นโค้งการสร้างรายได้จริง

    สคีมาความก้าวหน้าของฟันเนลและทิศทางของค่า

    ใน SKAdNetwork 3 และรุ่นก่อนหน้า Apple กำหนดให้ค่า conversion ต้องเพิ่มขึ้นอย่างต่อเนื่อง (monotonic) แต่ใน SKAdNetwork 4.0 Apple ได้ยกเลิกข้อจำกัดนี้ ทำให้ค่า conversion ในหน้าต่างที่ 1 สามารถเพิ่มขึ้นหรือลดลงข้ามการเรียก API ครั้งต่อๆ ไปได้

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

    • ค่า 0: ติดตั้งและเปิดแอปแล้ว
    • ค่า 10: ลงทะเบียนสำเร็จ
    • ค่า 25: จบการสอนใช้งาน (Onboarding)
    • ค่า 30: เพิ่มวิธีการชำระเงินแล้ว
    • ค่า 45: เพิ่มสินค้าลงตะกร้าแล้ว
    • ค่า 63: ชำระเงินครั้งแรกสำเร็จ

    สคีมาหมวดหมู่แบบบิตส์ (Bitwise Categorical Schemas)

    สคีมาแบบบิตส์จะปฏิบัติต่อจำนวนเต็ม 6 บิต (26=642^6 = 64) เสมือนเป็นแฟล็กบูลีนอิสระ 6 ตัว (b5b4b3b2b1b0b_5 b_4 b_3 b_2 b_1 b_0):

    ตำแหน่งบิต น้ำหนักไบนารี พฤติกรรมในแอปที่แมป
    บิต 0 (b0b_0) 1 (0b000001) ผู้ใช้ลงทะเบียนเสร็จสิ้น
    บิต 1 (b1b_1) 2 (0b000010) ผู้ใช้เปิดใช้งานการแจ้งเตือนแบบพุช
    บิต 2 (b2b_2) 4 (0b000100) ผู้ใช้เพิ่มสินค้าลงในรายการโปรด
    บิต 3 (b3b_3) 8 (0b001000) ผู้ใช้แชร์ลิงก์แนะนำเพื่อน
    บิต 4 (b4b_4) 16 (0b010000) ผู้ใช้ทำการซื้อสินค้าในแอปสำเร็จ
    บิต 5 (b5b_5) 32 (0b100000) ผู้ใช้สมัครสมาชิกช่วงทดลองใช้งานพรีเมียม

    เพย์โหลด JSON ที่มีการจัดเวอร์ชันด้านล่างแสดงตัวอย่างเอกสารการกำหนดค่าแบบไดนามิกหลายหน้าต่าง:

    {
      "schema_version": "4.0.1",
      "app_id": "1234567890",
      "currency": "USD",
      "windows": {
        "window_1": {
          "mode": "hybrid_revenue_and_funnel",
          "fine_mapping": [
            { "event": "app_open", "min_revenue_cents": 0, "fine_value": 0, "lock": false },
            { "event": "registration_complete", "min_revenue_cents": 0, "fine_value": 10, "lock": false },
            { "event": "tutorial_complete", "min_revenue_cents": 0, "fine_value": 20, "lock": false },
            { "event": "purchase", "min_revenue_cents": 99, "fine_value": 30, "lock": false },
            { "event": "purchase", "min_revenue_cents": 999, "fine_value": 45, "lock": false },
            { "event": "purchase", "min_revenue_cents": 4999, "fine_value": 63, "lock": true }
          ],
          "coarse_mapping": {
            "low": { "events": ["app_open", "registration_complete"] },
            "medium": { "events": ["tutorial_complete"] },
            "high": { "events": ["purchase"] }
          }
        },
        "window_2": {
          "mode": "coarse_retention_and_monetization",
          "coarse_mapping": {
            "low": { "events": ["app_open"], "lock": false },
            "medium": { "events": ["session_milestone"], "lock": false },
            "high": { "events": ["repeat_purchase"], "lock": true }
          }
        },
        "window_3": {
          "mode": "coarse_long_tail_ltv",
          "coarse_mapping": {
            "low": { "events": ["app_open"], "lock": false },
            "medium": { "events": ["level_milestone"], "lock": false },
            "high": { "events": ["subscription_active"], "lock": true }
          }
        }
      }
    }
    

    การกำหนดค่า SDK แบบไดนามิก: การนำเข้าและการประเมินการกำหนดค่าระยะไกลขณะรันไทม์

    กลไกการประเมินกฎฝั่งไคลเอนต์

    SDK การระบุแหล่งที่มาจะประเมินกฎ conversion ในเครื่องภายในรันไทม์ของแอปพลิเคชัน:

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

    การจัดการสถานะออฟไลน์และการคงอยู่ของข้อมูลในเครื่อง

    เมื่อแอปพลิเคชันเปิดใช้งานในขณะออฟไลน์หรือในสภาพแวดล้อมเครือข่ายที่ไม่เสถียร:

    1. SDK จะเริ่มต้นแสตมป์เวลาการเปิดใช้งานครั้งแรกอย่างอิสระในการจัดเก็บถาวรในเครื่อง
    2. SDK จะโหลดสคีมาการกำหนดค่าที่ตรึงไว้จากการจัดเก็บถาวรในเครื่อง โดยตรวจสอบว่าเพย์โหลดที่แคชไว้ตรงกับเวอร์ชันสคีมาที่ตรึงไว้หรือไม่
    3. หากเกิดอีเวนต์ในแอปขึ้นในขณะออฟไลน์ SDK จะประเมินอีเวนต์เหล่านั้นเทียบกับชุดกฎที่แคชไว้และเรียกใช้ StoreKit update API ทันที
    4. การเตรียมการและการส่ง postback ยังคงจัดการโดยระบบและเป็นแบบอะิงโครนัส โดยแอปไม่ต้องส่ง postback ด้วยตนเอง

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

    import Foundation
    import StoreKit
    
    // MARK: - Schema Configuration Models
    
    struct SKANSchemaConfig: Codable {
        let schemaVersion: String
        let appId: String
        let currency: String
        let windows: SchemaWindows
    
        enum CodingKeys: String, CodingKey {
            case schemaVersion = "schema_version"
            case appId = "app_id"
            case currency, windows
        }
    }
    
    struct SchemaWindows: Codable {
        let window1: Window1Config
        let window2: WindowCoarseConfig
        let window3: WindowCoarseConfig
    
        enum CodingKeys: String, CodingKey {
            case window1 = "window_1"
            case window2 = "window_2"
            case window3 = "window_3"
        }
    }
    
    struct Window1Config: Codable {
        let mode: String
        let fineMapping: [FineRule]
        let coarseMapping: CoarseRuleGroup
    
        enum CodingKeys: String, CodingKey {
            case mode
            case fineMapping = "fine_mapping"
            case coarseMapping = "coarse_mapping"
        }
    }
    
    struct FineRule: Codable {
        let event: String
        let minRevenueCents: Int
        let fineValue: Int
        let lock: Bool
    
        enum CodingKeys: String, CodingKey {
            case event
            case minRevenueCents = "min_revenue_cents"
            case fineValue = "fine_value"
            case lock
        }
    }
    
    struct WindowCoarseConfig: Codable {
        let mode: String
        let coarseMapping: [String: CoarseRule]
    
        enum CodingKeys: String, CodingKey {
            case mode
            case coarseMapping = "coarse_mapping"
        }
    }
    
    struct CoarseRuleGroup: Codable {
        let low: CoarseRule
        let medium: CoarseRule
        let high: CoarseRule
    }
    
    struct CoarseRule: Codable {
        let events: [String]?
        let lock: Bool?
    }
    
    // MARK: - Multi-Window SKAN 4.0 Schema Engine
    
    final class SKANSchemaEngine {
    
        static let shared = SKANSchemaEngine()
        private init() {}
    
        private var activeSchema: SKANSchemaConfig?
        private var firstLaunchDate: Date?
        private var lockedWindows = Set<Int>()
        private var lastRecordedFineValue: Int = 0
        private var pinnedSchemaVersion: String?
    
        /// Initializes the first-launch timestamp anchor independently of remote configuration fetches
        func initializeLifecycleAnchor() {
            let defaults = UserDefaults.standard
            if let storedLaunch = defaults.object(forKey: "skan_first_launch_date") as? Date {
                self.firstLaunchDate = storedLaunch
            } else {
                let now = Date()
                self.firstLaunchDate = now
                defaults.set(now, forKey: "skan_first_launch_date")
            }
    
            let lockedArray = defaults.array(forKey: "skan_locked_windows") as? [Int] ?? []
            self.lockedWindows = Set(lockedArray)
            self.lastRecordedFineValue = defaults.integer(forKey: "skan_last_fine_value")
            self.pinnedSchemaVersion = defaults.string(forKey: "skan_pinned_schema_version")
    
            // Restore previously cached schema payload if it matches the pinned version
            if let pinnedVersion = self.pinnedSchemaVersion,
               let cachedData = defaults.data(forKey: "skan_cached_schema_payload"),
               let cachedSchema = try? JSONDecoder().decode(SKANSchemaConfig.self, from: cachedData),
               cachedSchema.schemaVersion == pinnedVersion {
                self.activeSchema = cachedSchema
            }
        }
    
        /// Loads active schema, persisting the pinned payload to maintain consistency across the 35-day lifecycle
        func configure(schema: SKANSchemaConfig) {
            let defaults = UserDefaults.standard
            if let pinned = pinnedSchemaVersion {
                // If already pinned, accept only schemas matching the pinned version
                if pinned == schema.schemaVersion {
                    self.activeSchema = schema
                    if let data = try? JSONEncoder().encode(schema) {
                        defaults.set(data, forKey: "skan_cached_schema_payload")
                    }
                }
            } else {
                // Pin the initial schema version for this lifecycle
                self.activeSchema = schema
                self.pinnedSchemaVersion = schema.schemaVersion
                defaults.set(schema.schemaVersion, forKey: "skan_pinned_schema_version")
                if let data = try? JSONEncoder().encode(schema) {
                    defaults.set(data, forKey: "skan_cached_schema_payload")
                }
            }
        }
    
        /// Determines the active conversion window based on elapsed time from first launch
        private var currentWindowIndex: Int {
            guard let firstLaunch = firstLaunchDate else { return 0 }
            let elapsedHours = Date().timeIntervalSince(firstLaunch) / 3600.0
    
            switch elapsedHours {
            case 0.0..<48.0:
                return 1
            case 48.0..<168.0:
                return 2
            case 168.0...840.0:
                return 3
            default:
                return 0 // Window closed (>35 days)
            }
        }
    
        /// Evaluates an in-app event against the active schema for the current window
        func trackEvent(name: String, revenueCents: Int = 0) {
            guard #available(iOS 16.1, *),
                  let schema = activeSchema else { return }
    
            let window = currentWindowIndex
            guard window >= 1 && window <= 3, !lockedWindows.contains(window) else { return }
    
            var targetFineValue: Int?
            var targetCoarseValue: SKAdNetwork.CoarseConversionValue?
            var shouldLock = false
            var matchedRule = false
    
            if window == 1 {
                // Window 1: Evaluate fine-grained rules with highest-threshold precedence
                let matchingFineRules = schema.windows.window1.fineMapping
                    .filter { $0.event == name && revenueCents >= $0.minRevenueCents }
                    .sorted { $0.minRevenueCents < $1.minRevenueCents }
    
                if let highestRule = matchingFineRules.last {
                    targetFineValue = highestRule.fineValue
                    if highestRule.lock { shouldLock = true }
                    matchedRule = true
                }
    
                // Window 1: Evaluate coarse-grained rules explicitly
                if schema.windows.window1.coarseMapping.high.events?.contains(name) == true {
                    targetCoarseValue = .high
                    matchedRule = true
                } else if schema.windows.window1.coarseMapping.medium.events?.contains(name) == true {
                    targetCoarseValue = .medium
                    matchedRule = true
                } else if schema.windows.window1.coarseMapping.low.events?.contains(name) == true {
                    targetCoarseValue = .low
                    matchedRule = true
                }
            } else {
                // Windows 2 & 3: Evaluate coarse rules only
                let coarseConfig = (window == 2) ? schema.windows.window2 : schema.windows.window3
                
                if let highRule = coarseConfig.coarseMapping["high"], highRule.events?.contains(name) == true {
                    targetCoarseValue = .high
                    if highRule.lock == true { shouldLock = true }
                    matchedRule = true
                } else if let medRule = coarseConfig.coarseMapping["medium"], medRule.events?.contains(name) == true {
                    targetCoarseValue = .medium
                    if medRule.lock == true { shouldLock = true }
                    matchedRule = true
                } else if let lowRule = coarseConfig.coarseMapping["low"], lowRule.events?.contains(name) == true {
                    targetCoarseValue = .low
                    if lowRule.lock == true { shouldLock = true }
                    matchedRule = true
                }
            }
    
            // If no explicit rule matched for this event, do not trigger a StoreKit update
            guard matchedRule else { return }
    
            let fineToSubmit = targetFineValue ?? (window == 1 ? lastRecordedFineValue : 0)
            let clampedFine = max(0, min(63, fineToSubmit))
            let coarseToSubmit = targetCoarseValue ?? .low
    
            // Dispatch StoreKit conversion update
            // Note: StoreKit ignores the fineValue parameter after Window 1
            SKAdNetwork.updatePostbackConversionValue(
                clampedFine,
                coarseValue: coarseToSubmit,
                lockWindow: shouldLock
            ) { [weak self] error in
                guard let self = self else { return }
                if let error = error {
                    print("StoreKit conversion update failed: \(error.localizedDescription)")
                } else {
                    // Commit local state only after StoreKit successfully accepts the update
                    DispatchQueue.main.async {
                        if window == 1 {
                            self.lastRecordedFineValue = clampedFine
                            UserDefaults.standard.set(clampedFine, forKey: "skan_last_fine_value")
                        }
                        if shouldLock {
                            self.lockedWindows.insert(window)
                            UserDefaults.standard.set(Array(self.lockedWindows), forKey: "skan_locked_windows")
                        }
                        print("SKAN 4.0 update succeeded: Window=\(window), Fine=\(clampedFine), Coarse=\(coarseToSubmit.rawValue), Locked=\(shouldLock)")
                    }
                }
            }
        }
    }

    ระยะเวลาของ lockWindow ใน SKAN และการเตรียม postback ล่วงหน้า

    การทำให้อยู่ในรูปแบบอัตโนมัติของการดำเนินการ lockWindow เพื่อเร่งการเตรียม Postback

    กลไกการทำงานของพารามิเตอร์ lockWindow

    เมื่อแอปเรียกใช้ updatePostbackConversionValue(_:coarseValue:lockWindow:) พร้อมกับกำหนด lockWindow: true การอัปเดตนั้นจะกลายเป็นการอัปเดต conversion value ครั้งสุดท้ายสำหรับหน้าต่างที่ใช้งานอยู่ ระบบปฏิบัติการจะเตรียม postback ทันทีและจะไม่สนใจการอัปเดต conversion value เพิ่มเติมตลอดช่วงเวลาที่เหลือของหน้าต่างนั้น

    Default Window 1 (No Lock):
    [First Launch] ─────────────── 48 Hours Open ───────────────► [Closes] ──► Delay (24-48h) ──► Postback 1
    
    Locked Window 1 (Purchase at Hour 6):
    [First Launch] ── 6h (Lock: true) ──► [Conversion Locked / Postback Prepared] ──► Delay (24-48h) ──► Postback 1 Sent Sooner
    

    ข้อดีข้อเสียเชิงกลยุทธ์ในการล็อกหน้าต่างอัตโนมัติ

    • การส่ง Postback ที่เร็วขึ้น: การสรุปผล conversion เร็วกว่ากำหนดช่วยให้การหน่วงเวลา postback แบบสุ่มของ Apple เริ่มต้นได้ทันที ส่งผลให้ข้อมูล conversion ถูกส่งไปยังเครือข่ายโฆษณาได้เร็วขึ้น
    • ความเป็นอิสระของหน้าต่าง: การล็อกหน้าต่างปัจจุบันจะไม่เลื่อนเวลาเริ่มต้นของหน้าต่างถัดไปให้เร็วขึ้น หน้าต่างที่ 2 จะยังคงเริ่มต้นในวันที่ 3 โดยไม่คำนึงว่าหน้าต่างที่ 1 จะถูกล็อกเมื่อใด
    • การตัดทอนช่วงเวลาการสังเกตการณ์: เมื่อหน้าต่างถูกล็อกแล้ว ระบบจะละเว้นการเรียกอัปเดต conversion value เพิ่มเติมสำหรับช่วงเวลาที่เหลือของหน้าต่าง conversion นั้น อีเวนต์ในแอปอาจยังคงเกิดขึ้นต่อไปได้ แต่จะไม่สามารถเปลี่ยนแปลงสถานะ conversion SKAdNetwork ของหน้าต่างนั้นได้อีก

    การประสานสคีมา SKAN กับ AdAttributionKit

    สถาปัตยกรรมการระบุแหล่งที่มาที่กำลังพัฒนาของ Apple

    ปัจจุบัน Apple แนะนำให้ใช้ AdAttributionKit สำหรับแคมเปญการตลาดแอปข้าม App Store และตลาดแอปทางเลือกอื่นๆ ส่วน SKAdNetwork ยังคงมีความสำคัญสำหรับการบูรณาการที่มีอยู่และความสามารถในการทำงานร่วมกัน ดังนั้น เอ็นจิ้นการแมปแบบไดนามิกจึงควรแยกเลเยอร์กฎธุรกิจออกจาก conversion API เฉพาะของแต่ละเฟรมเวิร์ก:

    • มิติข้อมูลค่าที่ใช้ร่วมกัน: ทั้งสองเฟรมเวิร์กประเมินค่าละเอียด 6 บิต (0 ถึง 63) และค่าหยาบ 3 ระดับ (low, medium, high)
    • เลเยอร์ API ที่แตกต่างกัน: SKAdNetwork ใช้ SKAdNetwork.updatePostbackConversionValue ในขณะที่ AdAttributionKit ใช้ Postback.updateConversionValue
    • พฤติกรรมการเชื่อมโยง: หากการบูรณาการรองรับทั้งสองเฟรมเวิร์ก Apple แนะนำให้เรียกใช้ conversion-update API ของทั้งสองเฟรมเวิร์ก พร้อมทั้งคำนึงถึงพฤติกรรมการเชื่อมโยงระหว่าง SKAdNetwork กับ AdAttributionKit ตามที่มีการบันทึกไว้

    เมทริกซ์การตัดสินใจเปรียบเทียบ: ลอจิกฝั่งไคลเอนต์แบบฮาร์ดโค้ดเทียบกับการกำหนดค่าแบบไดนามิก

    มิติการประเมิน ลอจิกฝั่งไคลเอนต์แบบฮาร์ดโค้ด การกำหนดค่าสคีมาแบบไดนามิก
    ความเร็วในการปรับเปลี่ยนสคีมา ต้องผ่านการตรวจสอบของ App Store (ใช้เวลาหลายวันถึงหลายสัปดาห์) อัปเดตระยะไกลสำหรับกฎที่รองรับได้โดยไม่ต้องออกรุ่นไบนารีใหม่
    ความคล่องตัวในการทดสอบและวนซ้ำ มีแรงเสียดทานสูง / มีภาระงานทางวิศวกรรมสูง การทดลองสคีมาแบบควบคุม พร้อมกฎที่แยกตามเวอร์ชันและกลุ่ม cohorts
    การประสานงานหลายหน้าต่าง State Machine แบบ manual ที่ซับซ้อนใน Swift เอ็นจิ้นที่ตระหนักถึงวงจรชีวิตแบบอัตโนมัติ
    การล็อกหน้าต่างอัตโนมัติ ทริกเกอร์กฎแบบตายตัวและไม่ยืดหยุ่น กฎการล็อกแบบไดนามิกที่ทริกเกอร์ด้วยอีเวนต์
    ความเท่าเทียมกันข้ามเฟรมเวิร์ก โค้ดกระจัดกระจายไปตามเฟรมเวิร์กต่างๆ เมทริกซ์การกำหนดค่าบนคลาวด์แบบรวมศูนย์

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

    จะเกิดอะไรขึ้นหากผู้ใช้ทริกเกอร์หลายอีเวนต์ที่แมปกับ conversion value ที่แตกต่างกัน?
    ใน SKAdNetwork 4.0 Apple อนุญาตให้ conversion value ในหน้าต่างที่ 1 สามารถเพิ่มขึ้นหรือลดลงข้ามการเรียกใช้งานต่อเนื่องได้ อย่างไรก็ตาม สคีมาการระบุแหล่งที่มาอาจบังคับให้เกิดความก้าวหน้าอย่างต่อเนื่องตามธรรมเนียมการออกแบบของผู้จำหน่าย ซึ่งในกรณีนี้ SDK ของไคลเอนต์จะอัปเดต conversion value เฉพาะเมื่ออีเวนต์ที่เข้ามาสร้างค่าที่สูงกว่าสถานะที่บันทึกไว้ปัจจุบันเท่านั้น
    สคีมาอัตโนมัติสามารถอัปเดต conversion value ได้หรือไม่หากแอปออฟไลน์อยู่?
    ได้ หาก SDK มีสคีมาที่แคชไว้อย่างถูกต้อง SDK จะสามารถประเมินอีเวนต์และเรียกใช้ StoreKit ได้โดยไม่ต้องดึงข้อมูลสคีมาใหม่แบบซิงโครนัส การเตรียมการและการส่ง postback ของ SKAdNetwork จะยังคงถูกจัดการโดยระบบและเป็นแบบอะซิงโครนัสเช่นเดิม
    สคีมาอัตโนมัติจัดการกับการแปลงสกุลเงินสำหรับผู้ใช้ทั่วโลกอย่างไร?
    เอ็นจิ้นสคีมาอัตโนมัติจะทำการแปลงยอดซื้อในแอปทั้งหมดให้เป็นสกุลเงินฐานมาตรฐาน (เช่น เซนต์ดอลลาร์สหรัฐ) บนอุปกรณ์ หรือส่งค่าจำนวนเต็มที่แปลงล่วงหน้าก่อนที่จะประเมินเกณฑ์ของบักเก็ตรายได้

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

    การทำให้การแมป Conversion Value ของ SKAdNetwork เป็นแบบอัตโนมัติช่วยแยกการทดลองเกี่ยวกับการเติบโตออกจากวงจรการออกรุ่นไบนารีมือถือ ด้วยการแจกจ่ายสคีมาแบบไดนามิกจากแดชบอร์ดการระบุแหล่งที่มาส่วนกลางและประเมินสคีมาเหล่านั้นในเครื่องภายใน SDK ทีมวิศวกรจึงสามารถปรับแต่งบักเก็ตรายได้, ปรับปรุงขั้นตอนสำคัญของฟันเนล, และกำหนดค่าการล็อกหน้าต่างอัตโนมัติ ซึ่งช่วยให้การเปลี่ยนแปลงกฎ conversion ที่รองรับมีผลบังคับใช้ได้โดยไม่ต้องส่งไบนารีแอปพลิเคชันกลับไปยัง App Store Connect

    การกำหนดเส้นทางลิงก์เชิงลึกในระดับแอปพลิเคชัน (deep-link routing) สามารถทำงานควบคู่ไปกับเฟรมเวิร์กการระบุแหล่งที่มาที่รักษาความเป็นส่วนตัวของ Apple ในฐานะเลเยอร์การวัดผลและการเริ่มต้นใช้งานแยกต่างหาก แพลตฟอร์มอย่างเช่น OpoInstall มีโครงสร้างพื้นฐานสำหรับการกำหนดเส้นทางบริบทแบบเฟิร์สปาร์ตี้และการทำ deferred deep linking ซึ่งช่วยให้ทีมงานสามารถรักษาเจตนาของผู้ใช้ข้ามฟันเนล conversion จากเว็บสู่แอปได้

    หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการกำหนดค่าการระบุแหล่งที่มาที่ปฏิบัติตามกฎความเป็นส่วนตัวและไปพไลน์ deep linking สามารถศึกษาได้จาก เอกสารประกอบของ OpoInstall

    วัสดุอ้างอิงที่เกี่ยวข้อง

    • แนวคิด: Conversion Value Schemas, Dynamic Schema Mapping, Revenue Bucketing, Window Locking, Monotonicity

    • เทคโนโลยี: Apple SKAdNetwork, Apple AdAttributionKit, StoreKit Framework, OpoInstall Mobile SDK

    • มาตรฐาน: IETF RFC 8259 JSON Specification

    • API: StoreKit updatePostbackConversionValue API, AdAttributionKit Postback.updateConversionValue API

    เอกสารประกอบอย่างเป็นทางการ

    Share this article

    Keep Discovering

    OpenAI นำโฆษณามาสู่ ChatGPT ในอินเดีย? สิ่งที่เปลี่ยนไปสำหรับการตลาดแอปพลิเคชัน

    OpenAI นำโฆษณามาสู่ ChatGPT ในอินเดีย? สิ่งที่เปลี่ยนไปสำหรับการตลาดแอปพลิเคชัน

    OpenAI นำโฆษณามาสู่ ChatGPT ในอินเดียสำหรับแพ็กเกจ Free และ Go ค้นหาว่าโฆษณา AI แบบสนทนามีผลกระทบอย่างไรต่อการตลาดแอปมือถือและการทำ deferred deep linking

    Google เปิดตัว 4K Gemini Omni แล้ว? สิ่งที่จะเปลี่ยนไปสำหรับโฆษณาวิดีโอ

    Google เปิดตัว 4K Gemini Omni แล้ว? สิ่งที่จะเปลี่ยนไปสำหรับโฆษณาวิดีโอ

    Google เปิดตัว 4K Gemini Omni พร้อมฉากความยาวสูงสุด 40 วินาที ค้นพบว่าการสร้างวิดีโอด้วย AI ที่ปรับขนาดได้ส่งผลอย่างไรต่อการวัดผลแคมเปญและการattribution แบบ multi-touch

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

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

    เรียนรู้วิธีการวิเคราะห์แอปพลิเคชันมือถือในการติดตามการรักษาผู้ใช้ผ่านการแบ่งกลุ่มผู้ใช้ (Cohorts) การกำหนดเกณฑ์สถานะใช้งาน และการสร้างแบบจำลองการเสื่อมถอยของการรักษาผู้ใช้