App Store Operator event entegrasyon rehberi: iOS (Swift)
Durum: v1 taslağı, 2026-10-23. Burada anlatılan API planlanmış durumdadır ve henüz yayında değildir. API yayına girdiğinde bu satır kaldırılır.
Kimin için: Yerel bir Swift uygulamasına (SwiftUI veya UIKit) event izleme ekleyen iOS geliştiricileri ve bu belgeyi verdikleri yapay zekâ kodlama ajanları. Belge kendi içinde eksiksizdir: bir ajan entegrasyonu yalnızca bu dosyayla tamamlayabilir.
Yapay zekâ kodlama ajanları için: önce bunu okuyun
Bir iOS uygulamasına event izleme ekliyorsunuz; böylece uygulamanın sahibi App Store Operator'da funnel'lar oluşturabilecek. Uygulama event'leri X-Write-Key header'ıyla POST https://api.app-store-operator.com/v1/events adresine gönderecek.
Adımlar, sırasıyla
- Yazma anahtarını alın. Bu belgede hâlâ
YOUR_WRITE_KEYmetni geçiyorsa geliştiriciden anahtarını isteyin (Dashboard → uygulaması → Events → Setup; anahtaraso_wk_ile başlar). Asla bir anahtar uydurmayın ve yer tutucuyu kodda bırakmayın. - Tracker'ı ekleyin. §5 içindeki dosyayı ana uygulama target'ına
EventTracker.swiftadıyla, hiç değiştirmeden kopyalayın. Uygulama uzantılarına (widget, paylaşım, bildirim uzantıları) eklemeyin. - Uygulamaya bağlayın, §5.1 içindeki gibi: açılışta bir kez
configure(writeKey:), uygulama arka plana geçtiğindeEventTracker.flushInBackground(), açılışta ve arka plandan her dönüşteapp_opened. track(...)çağrılarını ekleyin: §6 içindeki event'lerden, akışı bu uygulamada bulunan her biri için. Akışı bulunmayan event'leri atlayın.identifyveresetçağrılarını yalnızca uygulamanın kendi oturum açma sistemi varsa ekleyin (§5.2).- Bitirirken §9 Tamamlanma kontrol listesi'ni uygulayın: her maddeyi tamamlandı, uygulanamaz veya beklemede olarak, bunu gösteren dosya ve satırla birlikte raporlayın.
Kurallar
EventTracker.swiftdosyasının davranışını (paketleme, yeniden deneme, endpoint, kuyruk) değiştirmeyin. Uygulamaya uymuyorsa yeniden yazmak yerine nedenini raporlayın.- Üçüncü taraf bağımlılık eklemeyin.
- Kişiyi doğrudan tanımlayan veri göndermeyin: ad, e-posta, telefon numarası, hassas konum, IDFA veya kullanıcının yazdığı metin (§7). Uygulama App Store'un Çocuklar (Kids) kategorisindeyse durun ve geliştiriciye söyleyin: bu tracker eklenmemelidir (§7.8). Uygulama analitik için kullanıcılardan onay istiyorsa, izlemenin bu onayı beklemesi gerekip gerekmediğini geliştiriciye sorun (§7.5).
- Özellik değerleri düzdür: string, sayı veya boolean. İç içe nesne veya dizi olmaz (§4.3).
- §6'daki event adlarını tam olarak yazıldığı gibi kullanın. §6'nın kapsamadığı bir akış için event adı vermeden önce geliştiriciye sorun.
identifyçağrısını yalnızca uygulamanın kendi kalıcı dahili kullanıcı kimliğiyle yapın; asla e-posta, telefon numarası veya adla yapmayın. Bunun hangi kimlik olduğunu anlayamıyorsanız sorun.- Bu dosyanın en üstündeki durum satırı bunu söylediği sürece API henüz yayında değildir. Bu durumda her şeyi kurup bağlayın, ancak §9'daki ağ kontrollerini beklemede olarak raporlayın; başarısız bir istek kodunuzdaki bir hata değildir.
Yazma anahtarı tasarım gereği herkese açıktır (yalnızca tek bir uygulama için event yazabilir), bu yüzden kaynak kodda veya Info.plist içinde durabilir.
1. Temel bilgiler
| Temel URL | https://api.app-store-operator.com/v1 |
| Kimlik doğrulama | Header X-Write-Key: <your write key> (Dashboard → uygulamanız → Events → Setup) |
| Event gönderme | POST /v1/events (1–100 event'lik paket) |
| Kullanıcı bağlama | Bir event hem anonymous_id hem user_id taşıdığında otomatik; ya da POST /v1/identify |
| İçerik türü | application/json, UTF-8 |
| Hız sınırı | Yazma anahtarı başına dakikada 10.000 event (tüm install'lar toplamı) ve install (anonymous_id) başına dakikada 300 event |
| Referans kod için en düşük iOS | iOS 15 (Swift concurrency, URLSession async API) |
| Veri saklama süresi | 13 ay |
2. Nasıl çalışır
- Uygulamanız ilk açılışta rastgele bir anonim kimlik oluşturur ve onu saklar.
- Önemsediğiniz her eylem bir event olur: bir ad, bir zaman, isteğe bağlı özellikler ve bazı device bağlamı bilgileri.
- Event'ler device'ta kuyruğa alınır ve paketler hâlinde gönderilir. Device çevrimdışıysa event'ler bekler ve daha sonra gönderilir.
- Kullanıcı sizin uygulamanızda oturum açtığında bir kullanıcı kimliği atayabilirsiniz. O andan itibaren event'ler iki kimliği de taşır ve App Store Operator önceki anonim event'lerle sonraki oturum açılmış event'leri tek bir kişi olarak değerlendirir.
- Dashboard'da event'lerin canlı olarak geldiğini görür ve bunlardan funnel'lar oluşturursunuz:
onboarding_started → paywall_viewed → purchase_completedgibi, tekil kişi sayısıyla sayılan sıralı adımlar.
3. Yazma anahtarınızı alın
- App Store Operator Dashboard'unu açın → Uygulamalar → uygulamanız → Events → Setup.
- Yazma anahtarını kopyalayın (
aso_wk_ile başlar). - Anahtarı bir terminalden (macOS veya Linux) test edin. Komut her çalıştırmada yeni bir
event_idve o anki zamanı üretir, bu yüzden tekrar çalıştırabilirsiniz:
curl -sS https://api.app-store-operator.com/v1/events \
-H "Content-Type: application/json" \
-H "X-Write-Key: YOUR_WRITE_KEY" \
-d '{
"events": [{
"event_id": "'"$(uuidgen)"'",
"name": "integration_test",
"anonymous_id": "curl-test",
"occurred_at": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
"properties": { "source": "curl" }
}]
}'Beklenen yanıt: accepted değeri 1, rejected boş. Event birkaç saniye içinde Events → Canlı altında görünür.
Neden yeni değerler: tekrarlanan bir event_id yalnızca bir kez saklanır, bu yüzden aynı kimlikle ikinci çalıştırma Canlı'da yeni bir şey göstermez; 7 günden eski bir zaman ise event'i aldığımız zamanla değiştirilir.
3.1 Anahtarı yenileme
Anahtarı yalnızca kötüye kullanılıyorsa yenileyin; örneğin biri anahtarla sahte event'ler gönderiyorsa. Anahtar zaten herkese açıktır, bu yüzden uygulamanızda görünür olması yenileme nedeni değildir.
- Setup → Anahtarı yenile. Bunu çalışma alanının her üyesi yapabilir ve işlem kayda geçer.
- Yeni anahtarı uygulamanıza koyun ve bir güncelleme yayınlayın.
- Eski anahtar 30 gün daha çalışır; böylece kullanıcılarınızın güncellemeye vakti olur. Bu süreden sonra hâlâ eski anahtarı kullanan build'ler
401alır ve referans kod bunların event'lerini atar. Bu build'leri kullanan kişiler güncelleyene kadar sayılmaz. - Eski anahtar hâlâ kötüye kullanılıyorsa Setup → Eski anahtarı şimdi iptal et 30 günlük süresini hemen bitirir. O andan itibaren eski anahtarı kullanan build'ler sayılmaz. Bunu çalışma alanının her üyesi yapabilir ve işlem kayda geçer.
- Eski bir anahtar 30 günlük süresi içindeyken Anahtarı yenile devre dışıdır. Yeniden yenilemek için önce eski anahtarı iptal edin veya 30 gününün dolmasını bekleyin.
4. API referansı
4.1 POST /v1/events
1 ile 100 arasında event gönderir.
Header'lar
| Header | Zorunlu | Değer |
|---|---|---|
Content-Type |
evet | application/json |
X-Write-Key |
evet | uygulamanızın yazma anahtarı |
Gövde
{
"events": [ <event object>, ... ]
}Gövdenin tamamı en fazla 1 MB olmalıdır. Gövdedeki veya bir event'teki bilinmeyen alanlar yok sayılır.
4.2 Yanıt
Paketin kendisi geçerli olduğu sürece, bazı event'ler reddedilmiş olsa bile 200 OK:
{
"accepted": 2,
"rejected": [
{ "index": 2, "event_id": "…", "code": "invalid_name", "message": "Event name must match ^[A-Za-z0-9_.:-]{1,64}$" }
],
"clamped": [
{ "index": 0, "event_id": "…" }
]
}accepted: saklanan event'ler; daha önce saklanmış yinelenen event'ler de dahil (bkz.event_id).rejected: saklanmayan ve değiştirilmeden yeniden denenmemesi gereken event'ler.clamped:occurred_atdeğeri aralık dışında olduğu için sunucunun alış zamanıyla değiştirilen event'ler. Bunlar saklanır veacceptediçinde sayılır.
Üç alan da her zaman bulunur; rejected ve clamped boş olabilir.
4.3 Event nesnesi
| Alan | Tür | Zorunlu | Kurallar |
|---|---|---|---|
event_id |
string (UUID) | evet | Her event için benzersiz. Gönderilirken değil, event oluşturulurken üretin; böylece yeniden denenen bir paket iki kez sayılmaz. |
name |
string | evet | ^[A-Za-z0-9_.:-]{1,64}$. Bir $ ve ardından bu karakterlerden 1–63 tanesi (ör. $session_start) App Store Operator'a ayrılmıştır ve reserved_name ile reddedilir; geçersiz diğer her ad invalid_name olur. snake_case object_action biçimini kullanın, ör. paywall_viewed. |
anonymous_id |
string | evet | 1–128 karakter. Her install için sabit. |
user_id |
string veya null | hayır | 1–128 karakter. Kendi kalıcı kullanıcı kimliğiniz. Asla e-posta veya telefon numarası değil. |
occurred_at |
string | evet | Saat dilimi içeren ISO 8601, ör. 2026-10-23T12:00:00.123Z. 7 gün öncesinden 5 dakika sonrasına kadar kabul edilir. |
properties |
object | hayır | Düz nesne, en fazla 50 anahtar. Anahtar: 1–64 karakter. Değer: string (≤ 1024 karakter), sayı, boolean veya null. İç içe nesne veya dizi olmaz. |
context |
object | hayır | Aşağıya bakın. Önerilir: Dashboard funnel'ları bu alanlara göre filtreler ve kırılımlara ayırır. |
context
| Alan | Örnek | Notlar |
|---|---|---|
app_version |
"2.4.0" |
CFBundleShortVersionString |
build |
"412" |
CFBundleVersion |
os_version |
"18.1" |
|
device_model |
"iPhone16,2" |
Donanım tanımlayıcısı, pazarlama adı değil |
locale |
"en_US" |
Device'ın dil ve bölge ayarı, language_REGION |
environment |
"app_store" |
"debug", "testflight" veya "app_store". Siz başka bir seçim yapmadıkça Dashboard yalnızca app_store event'lerini (ve bu alanı olmayan event'leri) gösterir |
Her context değeri en fazla 64 karakterlik bir string'dir. Bu kurala uymayan bir değer ve bu tabloda olmayan her anahtar, event reddedilmeden atılır.
Sunucu, isteğin ağ konumundan country alanını ekler. IP adresinin kendisi asla saklanmaz.
Serileştirilmiş bir event en fazla 8 KB olmalıdır.
Tam örnek
{
"event_id": "0b6f6a3e-3c3e-4d8a-8f6e-1f9b8a2c7d10",
"name": "purchase_completed",
"anonymous_id": "C5E1B0B2-4F7A-4E0F-9C1D-2A6B7E8F9012",
"user_id": "u_81723",
"occurred_at": "2026-10-23T12:04:31.512Z",
"properties": {
"product_id": "pro_yearly",
"price": 39.99,
"currency": "USD",
"trial": true
},
"context": {
"app_version": "2.4.0",
"build": "412",
"os_version": "18.1",
"device_model": "iPhone16,2",
"locale": "en_US",
"environment": "app_store"
}
}4.4 POST /v1/identify (isteğe bağlı)
Event göndermeden bir anonim kimliği bir kullanıcı kimliğine bağlar. Buna nadiren ihtiyaç duyarsınız: iki kimliği de taşıyan her event aynı işi yapar. Bağlantı sunucunuzda biliniyorsa kullanın.
{ "anonymous_id": "C5E1B0B2-4F7A-4E0F-9C1D-2A6B7E8F9012", "user_id": "u_81723" }Yanıt: 204 No Content. Bir anonim kimlik için ilk bağlantı geçerli olur; aynı anonim kimlik için sonradan gelen farklı kullanıcı kimlikleri yok sayılır.
4.5 Hatalar
| Durum | code |
Anlamı | İstemcinin yapacağı |
|---|---|---|---|
| 400 | invalid_body |
Gövde geçerli bir JSON veya { "events": [...] } değil, ya da 0 / 100'den fazla event var |
Kodu düzeltin; yeniden denemeyin |
| 401 | invalid_write_key |
Eksik, bilinmeyen veya iptal edilmiş anahtar | Yeniden denemeyin; paketi atın ve anahtarı kontrol edin |
| 413 | payload_too_large |
Gövde 1 MB'tan büyük | Daha küçük paketler gönderin |
| 429 | rate_limited |
Anahtar için dakikada 10.000 event ya da tek bir anonymous_id için 300 event aşıldı. İstekteki hiçbir şey saklanmadı |
Paketi tutun; Retry-After saniye sonra yeniden deneyin |
| 5xx | server_error |
Bizden kaynaklı hata | Paketi tutun; artan bekleme (backoff) ile yeniden deneyin |
Hata gövdesi: { "code": "invalid_write_key", "message": "…" }.
Event bazında ret kodları (200 yanıtının içinde):
| Kod | Ne zaman |
|---|---|
invalid_event |
events içindeki öğe bir JSON nesnesi değil |
invalid_name |
name eksik veya ^[A-Za-z0-9_.:-]{1,64}$ ile eşleşmiyor |
reserved_name |
name, $ ve ardından [A-Za-z0-9_.:-] kümesinden 1–63 karakterden oluşuyor |
invalid_id |
event_id eksik veya UUID değil; anonymous_id eksik veya 1–128 karakterlik bir string değil; user_id var ama 1–128 karakterlik bir string veya null değil |
invalid_timestamp |
occurred_at eksik, ayrıştırılamıyor veya saat dilimi içermiyor. (Aralık dışında olmak ret nedeni değildir: bkz. clamped) |
invalid_properties |
properties düz bir nesne değil; bir anahtar 1–64 karakter aralığının dışında; iç içe bir nesne veya dizi var; bir string 1.024 karakterden uzun |
too_many_properties |
50'den fazla özellik |
event_too_large |
Serileştirilmiş event 8 KB'tan büyük |
Tek satırda yeniden deneme kuralı: ağ hatasında, 429 ve 5xx durumlarında aynı event_id'leri koruyarak yeniden deneyin; diğer her durumda paketi atın.
5. Referans kod
Tek dosya, bağımlılık yok, iOS 15+, Swift 5 ve Swift 6 dil modlarında derlenir. Ana uygulama target'ına EventTracker.swift olarak ekleyin ve değiştirmeden kullanın.
Ne yapar:
- İlk izlenen event'le birlikte rastgele bir anonim kimlik oluşturur ve onu (
UserDefaultsiçinde) saklar.currentAnonymousIdbu kimliği döndürür. - Bir açma/kapama anahtarı vardır:
setEnabled(_:). Seçim açılışlar arasında hatırlanır ve varsayılan olarak açıktır. Kapatmak kimlikleri ve gönderilmemiş tüm event'leri siler; yeniden açılana kadartrackhiçbir şey yapmaz. - Her event'i diskteki bir kuyruğa kaydeder; böylece event'ler uygulama kapansa bile kaybolmaz.
- Kuyruğu en fazla 100 event'lik paketler hâlinde gönderir: her 30 saniyede bir, 20 veya daha fazla event beklediğinde ve siz
flush()çağırdığınızda. - Çevrimdışı veya
5xx: kuyruğu tutar ve yeniden denemeden önce bekler: 30 saniye, sonra 60, 120… en fazla 10 dakikaya kadar.429: kuyruğu tutar ve sunucununRetry-Afterdeğeri ne kadar diyorsa o kadar bekler. Diğer her yanıt: paketi kuyruktan çıkarır. - En fazla 1.000 event tutar; bu sınır aşılınca en eskiler atılır.
- Yalnızca Debug build'lerde: reddedilen ve sınırlanan (clamped) event'leri ve atılan paketleri Xcode konsoluna
[EventTracker] …olarak yazdırır. Release build'ler hiçbir şey yazdırmaz. - Her event'i
debug,testflightveyaapp_storeolarak işaretler (StoreKit, iOS 16+); böylece test çalıştırmaları funnel'larınıza karışmaz. flushInBackground()iOS'tan ek süre ister; böylece uygulama askıya alınırken bile son event'ler gönderilir.
track, configure çağrılmadan önce de çalışır; event'ler bir anahtar ayarlanana kadar kuyrukta bekler.
Bir uygulama, bir anahtar. Dashboard'daki her uygulamanın kendi yazma anahtarı vardır. Her anahtarı yalnızca o uygulamanın binary'sinde kullanın.
Uygulama uzantılarında kullanılmaz. Uzantıların kendi depolama alanları vardır; bu yüzden her biri kendi anonim kimliğini alır ve ayrı bir kişi olarak sayılır. Yalnızca ana uygulamadan izleyin.
import Foundation
import StoreKit
#if canImport(UIKit)
import UIKit
#endif
/// Event'leri App Store Operator'a gönderir. Belgeler: https://app-store-operator.com/docs/events/ios
public actor EventTracker {
public static let shared = EventTracker()
public enum Value: Codable, Sendable, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral,
ExpressibleByFloatLiteral, ExpressibleByBooleanLiteral {
case string(String), int(Int), double(Double), bool(Bool)
public init(stringLiteral value: String) { self = .string(value) }
public init(integerLiteral value: Int) { self = .int(value) }
public init(floatLiteral value: Double) { self = .double(value) }
public init(booleanLiteral value: Bool) { self = .bool(value) }
public init(from decoder: Decoder) throws {
let c = try decoder.singleValueContainer()
if let v = try? c.decode(Bool.self) { self = .bool(v) }
else if let v = try? c.decode(Int.self) { self = .int(v) }
else if let v = try? c.decode(Double.self) { self = .double(v) }
else { self = .string(try c.decode(String.self)) }
}
public func encode(to encoder: Encoder) throws {
var c = encoder.singleValueContainer()
switch self {
case .string(let v): try c.encode(v)
case .int(let v): try c.encode(v)
case .double(let v): try c.encode(v)
case .bool(let v): try c.encode(v)
}
}
}
private struct Context: Codable, Sendable {
let appVersion, build, osVersion, deviceModel, locale: String
var environment: String?
enum CodingKeys: String, CodingKey {
case appVersion = "app_version", build, osVersion = "os_version",
deviceModel = "device_model", locale, environment
}
}
private struct Event: Codable, Sendable {
let eventId, name, anonymousId: String
let userId: String?
let occurredAt: String
let properties: [String: Value]
var context: Context
enum CodingKeys: String, CodingKey {
case eventId = "event_id", name, anonymousId = "anonymous_id", userId = "user_id",
occurredAt = "occurred_at", properties, context
}
}
private struct Batch: Encodable { let events: [Event] }
private let endpoint = URL(string: "https://api.app-store-operator.com/v1/events")!
private let maxBatch = 100
private let maxQueue = 1_000
private let flushAt = 20
private let anonymousIdKey = "aso.events.anonymousId"
private let userIdKey = "aso.events.userId"
private let disabledKey = "aso.events.disabled"
private let storeURL: URL
private let context: Context
private let timestamp: ISO8601DateFormatter
private var writeKey: String?
private var anonymousId: String?
private var isEnabled: Bool
private var userId: String?
private var queue: [Event]
private var isFlushing = false
private var timer: Task<Void, Never>?
private var retryAt: Date?
private var backoff: TimeInterval = 0
private var environment: String?
private init() {
let defaults = UserDefaults.standard
isEnabled = !defaults.bool(forKey: disabledKey)
anonymousId = defaults.string(forKey: anonymousIdKey) // ilk izlenen event'le oluşturulur
userId = defaults.string(forKey: userIdKey)
let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
storeURL = dir.appendingPathComponent("aso-events-queue.json")
queue = (try? JSONDecoder().decode([Event].self, from: Data(contentsOf: storeURL))) ?? []
let info = Bundle.main.infoDictionary ?? [:]
let os = ProcessInfo.processInfo.operatingSystemVersion
var system = utsname()
uname(&system)
let model = withUnsafeBytes(of: &system.machine) { raw in
String(decoding: raw.prefix { $0 != 0 }, as: UTF8.self)
}
context = Context(
appVersion: info["CFBundleShortVersionString"] as? String ?? "",
build: info["CFBundleVersion"] as? String ?? "",
osVersion: "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)",
deviceModel: model,
locale: Locale.current.identifier,
environment: nil // gönderim sırasında doldurulur, bkz. resolveEnvironment()
)
timestamp = ISO8601DateFormatter()
timestamp.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
}
/// Açılışta bir kez çağırın.
public func configure(writeKey: String) {
self.writeKey = writeKey
guard timer == nil else { return }
timer = Task {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 30_000_000_000)
await self.flush()
}
}
}
/// Bu install'ın kimliği; ilk izlenen event'ten önce ve izleme kapalıyken nil.
/// Kullanıcının verilerinin silinmesini isteyebilmesi için bir destek ekranında gösterin.
public var currentAnonymousId: String? { anonymousId }
/// İzlemeyi açar veya kapatır ve seçimi açılışlar arasında hatırlar. İzleme varsayılan olarak açıktır.
/// Kapatmak bu install'ın kimliklerini ve henüz gönderilmemiş tüm event'leri siler; ardından `track` hiçbir şey yapmaz.
/// Uygulamanız önce kullanıcının onayını almak zorundaysa, kullanıcı kabul edene kadar açılışta `setEnabled(false)` çağırın.
public func setEnabled(_ enabled: Bool) {
isEnabled = enabled
let defaults = UserDefaults.standard
defaults.set(!enabled, forKey: disabledKey)
guard !enabled else { return }
anonymousId = nil
userId = nil
queue.removeAll()
defaults.removeObject(forKey: anonymousIdKey)
defaults.removeObject(forKey: userIdKey)
try? FileManager.default.removeItem(at: storeURL)
}
/// Bir event'i kaydeder. Hafiftir; ağ için asla beklemez. İzleme kapalıyken hiçbir şey yapmaz.
public func track(_ name: String, _ properties: [String: Value] = [:]) {
guard isEnabled else { return }
let anonymousId = anonymousId ?? newAnonymousId()
queue.append(Event(
eventId: UUID().uuidString,
name: name,
anonymousId: anonymousId,
userId: userId,
occurredAt: timestamp.string(from: Date()),
properties: properties,
context: context
))
if queue.count > maxQueue { queue.removeFirst(queue.count - maxQueue) }
persist()
if queue.count >= flushAt { Task { await self.flush() } }
}
/// Kullanıcı uygulamanızda oturum açtıktan sonra çağırın. Kendi kalıcı kullanıcı kimliğinizi kullanın, asla e-posta değil.
public func identify(userId: String) {
guard isEnabled else { return }
self.userId = userId
UserDefaults.standard.set(userId, forKey: userIdKey)
}
/// Oturum kapatılırken çağırın: sonraki event'ler yeni bir anonim kişiye ait olur.
public func reset() {
userId = nil
anonymousId = nil
UserDefaults.standard.removeObject(forKey: userIdKey)
UserDefaults.standard.removeObject(forKey: anonymousIdKey)
}
private func newAnonymousId() -> String {
let id = UUID().uuidString
anonymousId = id
UserDefaults.standard.set(id, forKey: anonymousIdKey)
return id
}
/// Kuyruktaki event'leri gönderir. Bir yeniden deneme beklemesi sürerken hiçbir şey yapmaz.
public func flush() async {
guard isEnabled, !isFlushing, let writeKey, !queue.isEmpty else { return }
if let retryAt, retryAt > Date() { return }
isFlushing = true
defer { isFlushing = false }
let environment = await resolveEnvironment()
while !queue.isEmpty {
var batch = Array(queue.prefix(maxBatch))
for i in batch.indices where batch[i].context.environment == nil {
batch[i].context.environment = environment
}
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(writeKey, forHTTPHeaderField: "X-Write-Key")
request.httpBody = try? JSONEncoder().encode(Batch(events: batch))
guard let (data, response) = try? await URLSession.shared.data(for: request) else {
delayRetry(nil) // çevrimdışı: kuyruğu tut
return
}
guard isEnabled else { return } // gönderim sırasında kapatıldı: kuyruk zaten silindi
let http = response as? HTTPURLResponse
let status = http?.statusCode ?? 0
if status == 429 || status >= 500 {
let retryAfter = http?.value(forHTTPHeaderField: "Retry-After").flatMap(TimeInterval.init)
delayRetry(status == 429 ? retryAfter : nil) // kuyruğu tut
return
}
retryAt = nil
backoff = 0
#if DEBUG
report(status: status, body: data)
#endif
// 2xx: saklandı. Diğer 4xx: aynı paketi yeniden denemek başarılı olamaz.
let sent = Set(batch.map(\.eventId))
queue.removeAll { sent.contains($0.eventId) }
persist()
}
}
/// Sunucu belirtirse `seconds` kadar, aksi hâlde 30 sn, 60 sn, 120 sn… en fazla 10 dakika bekler.
private func delayRetry(_ seconds: TimeInterval?) {
backoff = min(max(backoff * 2, 30), 600)
retryAt = Date().addingTimeInterval(seconds ?? backoff)
}
private func persist() {
try? JSONEncoder().encode(queue).write(to: storeURL, options: .atomic)
}
#if DEBUG
/// Yalnızca Debug build'lerde: atılan paketleri ve reddedilen veya sınırlanan event'leri Xcode konsoluna yazdırır.
private func report(status: Int, body: Data) {
let json = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] ?? [:]
guard (200..<300).contains(status) else {
print("[EventTracker] HTTP \(status), batch dropped: \(json)")
return
}
for key in ["rejected", "clamped"] {
if let items = json[key] as? [Any], !items.isEmpty { print("[EventTracker] \(key): \(items)") }
}
}
#endif
/// "debug", "testflight" veya "app_store". Release build'ler StoreKit'e sorar (iOS 16+); iOS 15'te bilinmez kalır.
private func resolveEnvironment() async -> String? {
if let environment { return environment }
#if DEBUG
environment = "debug"
#else
if #available(iOS 16.0, macOS 13.0, *), let result = try? await AppTransaction.shared {
switch result.unsafePayloadValue.environment {
case .production: environment = "app_store"
case .sandbox: environment = "testflight"
case .xcode: environment = "debug"
default: break
}
}
#endif
return environment
}
}
#if canImport(UIKit)
extension EventTracker {
/// Uygulama arka plana geçtiğinde kuyruğu gönderir; bitirebilmek için iOS'tan süre ister.
@MainActor public static func flushInBackground() {
let task = BackgroundTask()
Task {
await shared.flush()
task.end()
}
}
}
@MainActor private final class BackgroundTask {
private var id = UIBackgroundTaskIdentifier.invalid
init() {
id = UIApplication.shared.beginBackgroundTask { [weak self] in self?.end() }
}
func end() {
guard id != .invalid else { return }
UIApplication.shared.endBackgroundTask(id)
id = .invalid
}
}
#endif5.1 Uygulamaya bağlama
import SwiftUI
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
@State private var wasInBackground = false
init() {
Task {
await EventTracker.shared.configure(writeKey: "YOUR_WRITE_KEY")
await EventTracker.shared.track("app_opened", ["source": "cold"])
}
}
var body: some Scene {
WindowGroup { ContentView() }
.onChange(of: scenePhase) { _, phase in // iOS 17+; iOS 15–16'da `{ phase in }` kullanın
switch phase {
case .background:
wasInBackground = true
EventTracker.flushInBackground()
case .active where wasInBackground:
wasInBackground = false
Task { await EventTracker.shared.track("app_opened", ["source": "warm"]) }
default:
break
}
}
}
}UIKit: application(_:didFinishLaunchingWithOptions:) içinde, yukarıdaki gibi Task { … } içinde configure çağırın ve app_opened event'ini source: "cold" ile izleyin. sceneDidEnterBackground(_:) içinde (scene kullanmıyorsanız applicationDidEnterBackground(_:) içinde) EventTracker.flushInBackground() çağırın. sceneWillEnterForeground(_:) içinde (veya applicationWillEnterForeground(_:) içinde) app_opened event'ini source: "warm" ile izleyin.
app_opened her .active durumunda değil, yalnızca arka plandan dönüşte gönderilir: sistem uyarıları, Denetim Merkezi ve Face ID de uygulamayı önce inaktif, sonra yeniden aktif yapar.
5.2 Event takibi
await EventTracker.shared.track("paywall_viewed", ["placement": "onboarding"])
// Senkron koddan (buton aksiyonları, delegate'ler):
Task { await EventTracker.shared.track("onboarding_completed", ["duration_s": 42]) }
// Uygulamanızda oturum açma / kapatmadan sonra:
await EventTracker.shared.identify(userId: user.id)
await EventTracker.shared.reset()
// Onay veya uygulama içi "Analitik verilerini paylaş" anahtarı: açılışta, configure ve ilk track çağrısından önce,
// kullanıcının mevcut seçimiyle çağırın; seçim her değiştiğinde yeniden çağırın.
await EventTracker.shared.setEnabled(userAllowsAnalytics)
// Destek ekranı: kullanıcının verilerinin silinmesini isteyebilmesi için bunu gösterin (henüz hiçbir şey izlenmediyse nil).
let installId = await EventTracker.shared.currentAnonymousIdİzleme onayı bekliyorsa, setEnabled çağrısını §5.1 içindeki açılış app_opened event'inden önce yapın; izleme kapalıyken o çağrı hiçbir şey yapmaz ve hiçbir kimlik oluşturulmaz.
Sabit değerler ("onboarding", 42, 39.99, true) özellik değerlerine kendiliğinden dönüşür. Değişkenleri ise sarmalayın: .string(name), .int(count), .double(amount), .bool(flag).
StoreKit 2 ile satın almalar. Satın almaları, kullanıcının satın al düğmesine dokunduğu satın alma akışında izleyin. Transaction.updates içinde izlemeyin; orası yenilemeleri ve diğer device'lardan yapılan satın almaları da iletir.
import StoreKit
func buy(_ product: Product, startsTrial: Bool) async {
let base: [String: EventTracker.Value] = ["product_id": .string(product.id)]
await EventTracker.shared.track("purchase_started", base)
do {
switch try await product.purchase() {
case .success(.verified):
await EventTracker.shared.track("purchase_completed", base.merging([
"price": .double(NSDecimalNumber(decimal: product.price).doubleValue),
"currency": .string(product.priceFormatStyle.currencyCode),
"trial": .bool(startsTrial),
]) { $1 })
if startsTrial { await EventTracker.shared.track("trial_started", base) }
case .userCancelled:
await EventTracker.shared.track("purchase_failed", base.merging(["reason": "cancelled"]) { $1 })
case .success(.unverified):
await EventTracker.shared.track("purchase_failed", base.merging(["reason": "failed"]) { $1 })
case .pending:
break // Ask to Buy veya ek onay: henüz tamamlanmadı, hiçbir şey izlemeyin
@unknown default:
break
}
} catch {
await EventTracker.shared.track("purchase_failed", base.merging(["reason": "failed"]) { $1 })
}
}startsTrial, paywall'unuzun zaten bildiği değerdir: bu satın alma ücretsiz bir trial başlatıyorsa true. Uygulama satın almayı RevenueCat veya başka bir kütüphane üzerinden yapıyorsa, aynı çağrıları o kütüphanenin satın alma metodunun etrafına koyun.
6. Hangi event izlenmeli
Funnel'lar, arkalarındaki event'ler kadar iyidir. Aşağıdaki adımlarla başlayın, sorular ortaya çıktıkça yenilerini ekleyin.
Adlandırma kuralları
snake_case,object_action, geçmiş zaman:paywall_viewed,purchase_completed.- Her eylem için tek bir ad. Varyasyonları addaki değil (
settings_paywall_viewed), özelliklerde tutun (placement: "settings"ilepaywall_viewed). - Ada asla kimlik, tarih veya kullanıcı verisi koymayın.
- Bir funnel'ı bir özelliğe göre bölmek istediğiniz yerde, o özelliğe serbest metin değil kısa bir sabit değer listesi verin (
plan: "yearly"). Dashboard en çok görülen 10 değeri gösterir, gerisini "Diğer" altında toplar. - Adlar ve özellik değerleri büyük/küçük harfe duyarlıdır:
Paywall_Viewedvepaywall_viewediki farklı event'tir.
Önerilen başlangıç seti
Akışı uygulamanızda bulunan event'leri izleyin; gerisini atlayın. isteğe bağlı olarak işaretlenen özellikler gönderilmeyebilir.
| Event | Ne zaman | Özellikler |
|---|---|---|
app_opened |
Uygulama açılışı ve arka plandan her dönüş (§5.1 bağlantısı bunu gönderir) | source ("cold" açılış, "warm" dönüş) |
onboarding_started |
İlk onboarding ekranı gösterildi | |
onboarding_step_viewed |
İlki dahil her onboarding ekranı | step (sayı, 1'den başlar), step_name |
onboarding_completed |
Son onboarding adımı tamamlandı | duration_s isteğe bağlı (sayı) |
signup_completed |
Uygulamanızda hesap oluşturuldu | method ("apple", "google", "email") |
paywall_viewed |
Paywall gösterildi | placement (nerede gösterildiği: "onboarding", "settings", "feature_gate"), paywall_id isteğe bağlı |
purchase_started |
Kullanıcı satın al düğmesine dokundu | product_id |
purchase_completed |
Satın alma başarılı ve doğrulandı, satın alma akışında (§5.2). Ücretsiz trial başlatan satın almalar da dahildir | product_id, price (sayı), currency, trial (boolean) |
purchase_failed |
Kullanıcı vazgeçti veya satın alma başarısız oldu. Ask to Buy (beklemede) ikisi de değildir: hiçbir şey izlemeyin | product_id, reason ("cancelled", "failed") |
trial_started |
Bir satın alma ücretsiz trial başlattığında purchase_completed event'ine ek olarak gönderilir |
product_id |
purchase_restored |
Satın almaları geri yükleme etkin bir subscription buldu | product_id isteğe bağlı |
| Temel eyleminiz | Uygulamanızın asıl var olma nedeni olan eylem. Adını o eyleme göre verin: scan_completed, workout_finished, photo_exported |
eylemi tanımlayan özellikler |
Uygulamadan gelen event'lerin gösteremedikleri. Yenilemeler, ücretli subscription'a dönüşen ücretsiz trial'lar, iptaller ve refund'lar Apple tarafında, genellikle uygulamanız kapalıyken gerçekleşir. Uygulama bunları hiç görmez; bu yüzden bu event'lerde yer almazlar ve buradaki funnel'lar satın almada veya trial başlangıcında biter.
Bu event'lerle kurulabilecek örnek funnel'lar
| Funnel | Adımlar | Zaman aralığı | Yanıtladığı soru |
|---|---|---|---|
| İlk session | onboarding_started → onboarding_completed → paywall_viewed (placement = onboarding) → purchase_completed |
1 gün | Yeni kullanıcıların kaçı paywall'a ulaşıp satın alıyor |
| Onboarding kaybı | onboarding_step_viewed (step = 1) → (step = 2) → (step = 3) → onboarding_completed |
1 gün | Hangi onboarding ekranı en çok kişi kaybediyor |
| Paywall dönüşümü | paywall_viewed → purchase_started → purchase_completed, placement kırılımıyla |
1 saat | Hangi paywall konumu en iyi dönüşüyor ve kullanıcılar fiyatta mı yoksa Apple'ın satın alma ekranında mı bırakıyor |
| Aktivasyon | onboarding_completed → scan_completed (temel eyleminiz) |
1 gün | Yeni kullanıcıların kaçı uygulamanın asıl amacına ulaşıyor |
7. Gizlilik
Bu bölüm hukuki danışmanlık değil, yol göstericidir. Uygulamanızın ne gönderdiğini ve kullanıcılarınızın nerede olduğunu siz bilirsiniz; bu yüzden son kararlar, tercihen kendi hukuk danışmanınızla birlikte, size aittir.
Bu bölümdeki her konu için gizlilik iletişim adresi: [email protected].
7.1 App Store Operator'a ulaşan veriler
- Event adları ve özellikleri.
anonymous_id: referans kodun, izleme açıkken izlenen ilk event'le oluşturduğu ve izleme kapatılana veyaresetçağrılana kadar o install için sakladığı rastgele bir UUID. IDFA veya diğer uygulamalarla paylaşılan başka bir tanımlayıcı değildir.user_id, yalnızcaidentifyçağırırsanız.- §4.3 içindeki device bağlamı: uygulama version'ı, build, işletim sistemi sürümü, device modeli, dil ve bölge ayarı ve build ortamı.
- İsteğin ağ konumundan çıkarılan ülke (bkz. §7.3).
Asla göndermeyin: ad, e-posta, telefon numarası, hassas konum, IDFA veya kullanıcının yazdığı serbest metin; event adlarında, özelliklerde veya user_id içinde. user_id, uygulamanızın kendi dahili kimliği olmalıdır (örneğin bir veritabanı anahtarı); e-posta, telefon numarası veya başka bir şirketin atadığı bir kimlik olmamalıdır.
7.2 Roller ve hizmet sağlayıcılar
- Roller. Uygulamanızın gönderdiği event'ler için veri sorumlusu (KVKK'daki adıyla veri sorumlusu) sizsiniz: neyin, neden toplanacağına siz karar verirsiniz. App Store Operator'ı işleten Nefarius, event'leri saklamak ve Dashboard'da size göstermek için sizin adınıza, veri işleyen (KVKK'daki adıyla veri işleyen) olarak işler.
- Yapmadıklarımız. Event verilerini satmayız, reklam için kullanmayız ve başka müşterilerle paylaşmayız.
- Hizmet sağlayıcılar (alt işleyenler). Her barındırılan hizmet gibi, event verilerini yalnızca bizim adımıza işleyen sağlayıcılardan yararlanırız:
- Cloudflare: ağ iletimi ve güvenlik. Uygulamanızdan gelen her istek bunun üzerinden geçer.
- Sunucu barındırma sağlayıcımız: sunucuları ve event'lerin saklandığı veritabanını çalıştırır.
- Konumlar. Cloudflare dünya genelinde çalışır; bu yüzden bir istek kullanıcılarınızın ülkesi dışında işlenebilir.
- Veri işleme sözleşmesi (DPA). Bir DPA'ya (örneğin GDPR madde 28 kapsamında), hizmet sağlayıcıların konumlarıyla birlikte güncel listesine veya uluslararası aktarımlara ilişkin güvencelerin ayrıntılarına ihtiyacınız varsa yukarıdaki gizlilik iletişim adresine yazın.
7.3 IP adresi ve ülke
Uygulamanızın istekleri bize Cloudflare üzerinden ulaşır; Cloudflare istekleri iletmek için IP adresine ihtiyaç duyar ve ülkeyi bu adresten belirler. App Store Operator yalnızca ülkeyi saklar (şehir veya bölge saklamaz) ve IP adresini asla saklamaz veya loglamaz.
7.4 Saklama süresi ve silme talepleri
- Event'ler 13 ay saklanır, ardından otomatik olarak silinir.
- Tek bir kişinin event'lerini Dashboard'dan silmek henüz mümkün değildir. Bu özellik gelene kadar, bir kullanıcı verilerinin silinmesini istediğinde (örneğin GDPR madde 17 veya KVKK madde 11 kapsamında), App Store Operator çalışma alanınızın bir üyesinin adresinden yukarıdaki gizlilik iletişim adresine şunlarla birlikte e-posta gönderin:
- Dashboard'da göründüğü şekliyle uygulamanın adı;
- kişinin
user_iddeğeri ve/veyaanonymous_iddeğeri. Biruser_id, ona bağlı heranonymous_iddeğerini de kapsar. Hesabı olmayan kullanıcılar için uygulamanızanonymous_iddeğeriniEventTracker.shared.currentAnonymousIdüzerinden, örneğin bir destek ekranında gösterebilir (§5.2). Bu değerisetEnabled(false)veyareset()çağırmadan önce okuyun: ikisi de değeri device'tan siler ve o olmadan o install'ın event'lerini bulamayız.
- Bu kimliklere sahip her event'i sileriz ve e-postayla onaylarız. Talepleri vakit kaybetmeden gönderin: kullanıcıya yanıt vermek için sizin de bir yasal süreniz vardır (GDPR kapsamında genellikle bir ay, KVKK kapsamında 30 gün).
- Çalışma alanının üyesi olmayan herhangi birinden gelen talepler işleme alınmaz; o kişiyi size yönlendiririz.
7.5 Gizlilik politikanız ve kullanıcı onayı
- Gizlilik politikası. Uygulamanızın gizlilik politikası; kullanım analitiği topladığınızı, bunun hangi verileri içerdiğini (§7.1), bu verileri sizin adınıza bir hizmet sağlayıcının (Nefarius tarafından işletilen App Store Operator) işlediğini ve verilerin 13 ay saklandığını belirtmelidir.
- Hukuki dayanak ve onay. Analitik için hukuki dayanağı ve önce kullanıcılarınızın onayına (açık rızasına) ihtiyaç duyup duymadığınızı siz belirlersiniz. AB ve Birleşik Krallık'ta analitik amacıyla device'ta bir tanımlayıcı saklamak veya okumak çoğu zaman onay gerektirir; KVKK kapsamında ise kullanıcıları en azından bilgilendirmeniz gerekir. Onaya ihtiyacınız varsa, açılışta her şeyden önce
EventTracker.shared.setEnabled(false)çağırın ve kullanıcı kabul edene kadar böyle bırakın, ardındansetEnabled(true)çağırın (§5.2). İzleme kapalıyken device'ta hiçbir kimlik veya event saklanmaz (yalnızca kapalı ayarının kendisi saklanır) ve hiçbir şey gönderilmez. Tracker,anonymous_iddeğerini yalnızca izleme açıkken izlenen ilk event'le oluşturur. Kullanıcı daha sonra onayını geri çekersesetEnabled(false)izlemeyi durdurur ve device'taki kimlikleri ve gönderilmemiş event'leri siler. Daha önce gönderilmiş event'leri silmez: tek hukuki dayanağınız onay idiyse veya kullanıcı silinmesini isterse, öncecurrentAnonymousIddeğerini okuyarak silmeyi §7.4 üzerinden talep edin.
7.6 App Tracking Transparency
App Store Operator event'lerinizi reklam veya reklam ölçümü amacıyla diğer şirketlerin uygulamalarından, web sitelerinden veya çevrimdışı kaynaklarından gelen verilerle birleştirmez, veri simsarlarıyla paylaşmaz ve device bağlamını device'ların parmak izini çıkarmak için kullanmaz. Bu nedenle entegrasyon tek başına Apple'ın tanımladığı anlamda izleme (tracking) sayılmaz ve App Tracking Transparency (ATT) iznini gerektirmez.
ATT kararı her entegrasyon için ayrı ayrı değil, uygulamanızın bütünü için verilir. Uygulamanız veya içindeki başka bir SDK bu event'leri ya da user_id değerinizi reklam veya reklam ölçümü amacıyla diğer şirketlerin verileriyle ilişkilendiriyorsa ya da bunları bir veri simsarıyla paylaşıyorsa, bu izlemedir: önce kullanıcının ATT iznini almanız gerekir ve gizlilik etiketiniz verilerin izleme için kullanıldığını belirtmelidir. Bkz. Apple'ın User Privacy and Data Use sayfası.
7.7 App Store gizlilik etiketi ve gizlilik manifesti
Apple'ın gizlilik sorularını uygulamanın bütünü için yanıtlarsınız ve Apple, eklediğiniz üçüncü taraf kodların topladığı verileri de dahil etmenizi ister; bu kod da buna dahildir. App Store Operator'a ulaşan veriler için tipik yanıtlar şunlardır:
| Apple veri türü | Ne zaman beyan edilir | Amaç |
|---|---|---|
| Usage Data → Product Interaction | Her zaman | Analytics |
| Identifiers → Device ID | Her zaman: anonymous_id install başına kalıcı bir tanımlayıcıdır |
Analytics |
| Location → Coarse Location | Her zaman: ülke, kullanıcının ağ konumundan çıkarılır. Apple'ın örneklerinden daha kaba bir konumdur, ancak beyan etmek daha güvenli yanıttır | Analytics |
| Purchases → Purchase History | §6'daki satın alma event'lerini izliyorsanız | Analytics |
| Identifiers → User ID | identify çağırıyorsanız veya user_id gönderiyorsanız |
Analytics |
- Linked to You.
user_idgönderiyorsanız yukarıdaki her satırı Linked to You olarak beyan edin: event'ler sisteminizdeki bir hesap kimliğiyle ilişkilendirilir. Hiçuser_idgöndermiyorsanız ve event'leri başka hiçbir yerde bir hesaba veya kimliği belirleyen başka verilere bağlamıyorsanız, veri yalnızca rastgele bir install kimliğine bağlıdır ve Not Linked to You yanıtını verebilirsiniz. Emin değilseniz Linked to You yanıtını verin. - Tracking. Uygulamanız §7.6'da anlatıldığı şekilde izleme yapmıyorsa Not used to track you.
- Gizlilik manifesti. Referans kod, Apple'ın kullanım gerekçesi istediği bir API olan
UserDefaultsüzerinde okuma ve yazma yapar. UygulamanızınPrivacyInfo.xcprivacydosyasıNSPrivacyAccessedAPICategoryUserDefaultsgirdisiniCA92.1gerekçesiyle listelemelidir (birçok uygulamada zaten vardır) ve dosyadaki toplanan veri türleri etiketinizle uyuşmalıdır.
Apple'ın tanımları: App privacy details on the App Store ve Privacy manifest files.
7.8 Çocuklara yönelik uygulamalar
Apple'ın App Review Guidelines kuralları (1.3 ve 5.1.4), hizmetin çocuklar hakkında device bilgileri dahil hiçbir tanımlayıcı bilgi toplamadığı sınırlı durumlar dışında, Kids kategorisindeki uygulamalarda üçüncü taraf analitiğe izin vermez. App Store Operator kalıcı bir install kimliği, device modeli ve ülke bilgisi alır; bu yüzden onu Kids kategorisindeki bir uygulamaya eklemeyin. Çocuklara yönelik diğer uygulamalar için entegrasyondan önce size uygulanan kuralları (örneğin ABD'de COPPA veya GDPR'ın çocukların onayına ilişkin kuralları) hukuk danışmanınızla birlikte kontrol edin.
8. Doğrulama ve sorun giderme
- Uygulamayı çalıştırın, birkaç event tetikleyin, sonra uygulamayı arka plana gönderin (bu, kuyruğu gönderir).
- Dashboard → uygulamanız → Events → Canlı: event'leriniz saniyeler içinde görünür.
- Events → Katalog her event adını özellik anahtarlarıyla birlikte listeler. Adların ve anahtarların kastettiğiniz gibi olduğunu kontrol edin.
Test build'leri. Her uygulamanın tek bir yazma anahtarı vardır; bu anahtarı test build'leri de App Store build'leri de kullanır. Referans kod her event'i debug, testflight veya app_store olarak işaretler. Dashboard varsayılan olarak app_store event'lerini ve ortam bilgisi olmayan event'leri gösterir; test çalıştırmalarınızı görmek için ortam filtresini Tüm build'ler olarak değiştirin. iOS 15'te Release build'ler TestFlight'ı App Store'dan ayırt edemez; bu yüzden event'leri App Store event'i sayılır. curl testi ortam bilgisi göndermez, bu yüzden varsayılan görünümde görünür; onu anonymous_id değeri olan curl-test ile bulabilirsiniz.
| Belirti | Olası neden |
|---|---|
| Canlı'da hiçbir şey yok | Yanlış anahtar (401); configure hiç çağrılmadı; ya da kuyrukta 20'den az event var ve uygulama henüz arka plana geçmedi (30 sn bekleyin) |
401 invalid_write_key |
Anahtarda yazım hatası; anahtar 30 günden daha önce yenilendi; ya da eski anahtar iptal edildi (§3.1) |
| Bir event eksik, diğerleri var | Reddedildi. Debug build'de Xcode konsolu kodla birlikte [EventTracker] rejected: … gösterir (§4.5) |
| Test build'inden Canlı'da hiçbir şey yok | Varsayılan görünüm App Store event'lerini ve ortam bilgisi olmayan event'leri gösterir: ortam filtresini Tüm build'ler olarak değiştirin |
| Event'ler gecikmeli ve toplu geliyor | Bir hız sınırı aşıldı (429) ya da sunucu veya ağ kesintideydi; referans kod event'leri tutar ve daha sonra gönderir |
| Zamanlar yanlış | Device saati çok sapmış: 7 günden daha eski veya 5 dakikadan daha ileri zamanlar, event'i aldığımız zamanla değiştirilir |
| Aynı kişi bir funnel'da iki kez sayılıyor | reset() çağrılmaması gereken bir yerde çağrılıyor ya da identify aynı kullanıcı için farklı kimliklerle çağrılıyor |
| Funnel adımı sıfır | Adımın event adı veya özellik filtresi tam olarak eşleşmiyor (adlar ve değerler büyük/küçük harfe duyarlıdır) |
9. Tamamlanma kontrol listesi
Her satır doğru olduğunda entegrasyon tamamlanmış olur. Rapor verirken her kod maddesi için dosya ve satırı belirtin; bir maddeyi gerekçesiyle uygulanamaz olarak, canlı API gerektiriyorsa beklemede olarak işaretleyin.
-
EventTracker.swiftyalnızca ana uygulama target'ında (uzantılarda değil), §5'tekinden hiç değiştirilmeden bulunuyor ve uygulama derleniyor. -
configure(writeKey:)açılışta bu uygulamanın gerçek anahtarıyla bir kez çalışıyor;YOUR_WRITE_KEYmetni projenin hiçbir yerinde geçmiyor. -
EventTracker.flushInBackground()uygulama arka plana geçtiğinde çalışıyor. -
app_openedaçılışta (source: "cold") ve arka plandan her dönüşte (source: "warm") izleniyor. - §6'daki event'lerden akışı bu uygulamada bulunan her biri, listelenen özelliklerle izleniyor. Her birini
event → file:lineolarak raporlayın ve atladığınız §6 event'lerini nedenleriyle listeleyin. - Satın alma event'leri
Transaction.updatesiçinde değil, satın alma akışında izleniyor. - Her event adı
^[A-Za-z0-9_.:-]{1,64}$ile eşleşiyor ve §6 adlandırma kurallarına uyuyor. - Hiçbir
trackçağrısı ad, e-posta, telefon numarası, hassas konum, IDFA veya kullanıcının yazdığı metin göndermiyor. - Uygulamada hesaplar varsa:
identifyoturum açıldıktan sonra uygulamanın kendi kullanıcı kimliğiyle,resetise oturum kapatıldıktan sonra çağrılıyor. Hesap yoksa ikisi de çağrılmıyor. - Canlı API: §3'teki curl testi
accepted: 1döndürüyor ve uygulamanın bir çalıştırmasından gelen event'ler Dashboard → Events → Canlı altında görünüyor. - Geliştirici: yukarıdaki event ve özellik listesi; §7'ye, App Store gizlilik etiketine,
PrivacyInfo.xcprivacyiçindekiUserDefaultsgirdisine, uygulamanın gizlilik politikasına ve varsa onay gerekliliğine göre kontrol etmesi için geliştiriciye teslim edildi. Uygulama Kids kategorisinde değil.