Giriş yap
Kodlama ajanları için Markdown

App Store Operator event entegrasyon rehberi: React Native

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: Bir React Native uygulamasının (bare React Native veya Expo) iOS build'ine event izleme ekleyen React Native geliştiricileri ve bu belgeyi verdikleri yapay zekâ kodlama ajanları. Belge kendi içinde eksiksizdir: bir ajan entegrasyonu yalnızca bu dosyayla tamamlayabilir. App Store Operator şimdilik iOS uygulamalarını kapsar: bu rehberdeki tracker Android'de hiçbir şey yapmaz (Android desteği planlanıyor).


Yapay zekâ kodlama ajanları için: önce bunu okuyun

Bir React Native uygulamasının iOS build'ine 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

  1. Yazma anahtarını alın. Bu belgede hâlâ YOUR_WRITE_KEY metni geçiyorsa geliştiriciden anahtarını isteyin (Dashboard → uygulaması → Events → Setup; anahtar aso_wk_ ile başlar). Asla bir anahtar uydurmayın ve yer tutucuyu kodda bırakmayın.
  2. Tracker'ı ekleyin. §5 içinde listelenen iki paketi kurun, ardından §5'teki dosyayı uygulamanın kaynak koduna eventTracker.ts adıyla, değiştirmeden kopyalayın.
  3. Uygulamaya bağlayın, §5.1 içindeki gibi: açılışta bir kez configure(...), uygulama arka plana geçtiğinde EventTracker.flush(), açılışta ve arka plandan her dönüşte app_opened.
  4. 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.
  5. identify ve reset çağrılarını yalnızca uygulamanın kendi oturum açma sistemi varsa ekleyin (§5.2).
  6. 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.ts dosyasının davranışını değiştirmeyin (paketleme, yeniden denemeler, endpoint, kuyruk, yalnızca iOS kontrolü). Uygulamaya uymuyorsa yeniden yazmak yerine nedenini raporlayın.
  • Başka hiçbir üçüncü taraf bağımlılık eklemeyin: yalnızca @react-native-async-storage/async-storage ve react-native-device-info. Uygulamada bunlar zaten varsa mevcut sürümlerini koruyun.
  • Tracker Android'de bilerek hiçbir şey yapmaz. Bunun için Android'e özel kod eklemeyin ve kontrolü atlatmaya çalışmayın.
  • 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, number, boolean veya null. İç içe nesne veya dizi olamaz (§4.3). Tracker'ın tipleri bunu zorunlu kılar; tip dönüştürmeyle (cast) bunu aşmayın.
  • §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 uygulamanın yapılandırmasında 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 asgari gereksinim React Native 0.76 veya Expo SDK 52, yalnızca iOS build; TypeScript veya JavaScript. Expo için Expo Go değil, bir development build gerekir
Veri saklama süresi 13 ay

2. Nasıl çalışır

  1. Uygulamanız ilk açılışta rastgele bir anonim kimlik oluşturur ve onu saklar.
  2. Önemsediğiniz her eylem bir event olur: bir ad, bir zaman, isteğe bağlı özellikler ve bazı device bağlamı bilgileri.
  3. 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.
  4. 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.
  5. Dashboard'da event'lerin canlı olarak geldiğini görür ve bunlardan funnel'lar oluşturursunuz: onboarding_started → paywall_viewed → purchase_completed gibi, tekil kişi sayısıyla sayılan sıralı adımlar.

3. Yazma anahtarınızı alın

  1. App Store Operator Dashboard'unu açın → Uygulamalar → uygulamanız → Events → Setup.
  2. Yazma anahtarını kopyalayın (aso_wk_ ile başlar).
  3. Anahtarı bir terminalden (macOS veya Linux) test edin. Komut her çalıştırmada yeni bir event_id ve o anki zamanı üretir, bu yüzden tekrar çalıştırabilirsiniz:
Terminal
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.

  1. Setup → Anahtarı yenile. Bunu çalışma alanının her üyesi yapabilir ve işlem kayda geçer.
  2. Yeni anahtarı uygulamanıza koyun ve bir güncelleme yayınlayın.
  3. 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 401 alır ve referans kod bunların event'lerini atar. Bu build'leri kullanan kişiler güncelleyene kadar sayılmaz.
  4. 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.
  5. 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

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

JSON
{
  "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_at değeri aralık dışında olduğu için sunucunun alış zamanıyla değiştirilen event'ler. Bunlar saklanır ve accepted iç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

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

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

Bare React Native veya Expo uygulamasının iOS build'i için tek bir TypeScript dosyası. Uygulamanın kaynak koduna eventTracker.ts olarak ekleyin ve değiştirmeden kullanın. İki pakete ihtiyaç duyar, başka hiçbir şeye değil.

Paketleri kurun

Bare React Native:

Terminal
npm install @react-native-async-storage/async-storage react-native-device-info
npx pod-install

Expo:

Terminal
npx expo install @react-native-async-storage/async-storage react-native-device-info
npx expo prebuild

İki paket de native kod içerir. Expo'da bir development build'de veya npx expo prebuild sonrasında çalışırlar; react-native-device-info içermeyen Expo Go'da çalışmazlar. Kurduktan sonra iOS uygulamasını yeniden derleyin. Uygulama bu paketlerden birini zaten kullanıyorsa sürümünü koruyun: tracker, @react-native-async-storage/async-storage paketinden (sürüm 2 veya 3) yalnızca getItem, setItem ve removeItem, react-native-device-info paketinden ise yalnızca getVersion, getBuildNumber, getSystemVersion ve getDeviceId kullanır.

Ne yapar:

  • Yalnızca iOS'ta çalışır. Diğer tüm platformlarda (Platform.OS !== "ios") her metot hiçbir şey yapmaz, böylece Android install'ları App Store funnel'larına karışmaz.
  • İlk izlenen event'le birlikte rastgele bir anonim kimlik (UUID v4) oluşturur ve saklar (AsyncStorage'da). currentAnonymousId() bu kimliği döndürür.
  • Açılışlar arasında hatırlanan ve varsayılan olarak açık olan bir açma/kapama anahtarı vardır: setEnabled(...). Kapatmak kimlikleri ve gönderilmemiş tüm event'leri siler; yeniden açılana kadar track hiçbir şey yapmaz.
  • Her event'i AsyncStorage'daki bir kuyruğa kaydeder, böylece event'ler uygulama kapansa da 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 sunucunun Retry-After değ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 geliştirme build'lerinde (__DEV__): reddedilen ve sınırlanan (clamped) event'leri ve atılan paketleri Metro / JS konsoluna [EventTracker] … olarak yazar. Release build'ler hiçbir şey yazmaz.
  • Geliştirme build'inden gelen her event'i debug olarak işaretler, böylece test çalıştırmaları funnel'larınızın dışında kalır. Bir release build, native kod olmadan TestFlight'ı App Store'dan ayırt edemez; bu yüzden configure, build yapılandırması bunu zaten bilen uygulamalar için isteğe bağlı bir environment ("testflight" veya "app_store") alır (§5.1). Bu değer verilmezse release event'leri ortam bilgisi taşımaz ve Dashboard onları App Store event'i olarak sayar.
  • flush(), uygulama arka plana geçtiğinde kuyruğu gönderir. iOS burada uygulamaya birkaç saniye tanır; o süre içinde gönderilemeyen her şey kuyrukta kalır ve bir sonraki açılışta gönderilir.

track, configure çağrılmadan önce ve kayıtlı durum AsyncStorage'dan yüklenmeden önce de çalışır; event'ler bir anahtar atanana kadar önce bellekte, sonra 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 (widget, paylaşım, bildirim uzantıları) 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 uygulamanın JavaScript kodundan izleyin.

TypeScript
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Platform } from "react-native";
import DeviceInfo from "react-native-device-info";

/** Event'leri App Store Operator'a gönderir. Belgeler: https://app-store-operator.com/docs/events/react-native */

/** Bir özellik değeri. Yalnızca düz: nesne veya dizi olamaz. */
export type EventValue = string | number | boolean | null;
export type EventProperties = Record<string, EventValue>;

export type EventEnvironment = "testflight" | "app_store";

type Context = {
  app_version: string;
  build: string;
  os_version: string;
  device_model: string;
  locale: string;
  environment?: string;
};

type Event = {
  event_id: string;
  name: string;
  anonymous_id: string;
  user_id: string | null;
  occurred_at: string;
  properties: EventProperties;
  context: Context;
};

const ENDPOINT = "https://api.app-store-operator.com/v1/events";
const MAX_BATCH = 100;
const MAX_QUEUE = 1_000;
const FLUSH_AT = 20;
const FLUSH_INTERVAL_MS = 30_000;
const REQUEST_TIMEOUT_MS = 60_000;
const ANONYMOUS_ID_KEY = "aso.events.anonymousId";
const USER_ID_KEY = "aso.events.userId";
const DISABLED_KEY = "aso.events.disabled";
const QUEUE_KEY = "aso.events.queue";

// App Store Operator iOS uygulamalarını kapsar. Diğer tüm platformlarda her metot hiçbir şey yapmaz.
const SUPPORTED = Platform.OS === "ios";

class Tracker {
  private writeKey: string | null = null;
  private environment: string | undefined = __DEV__ ? "debug" : undefined;
  private isEnabled = true;
  private anonymousId: string | null = null;
  private userId: string | null = null;
  private queue: Event[] = [];
  private isFlushing = false;
  private timer: ReturnType<typeof setInterval> | null = null;
  private retryAt = 0;
  private backoffSeconds = 0;
  private cachedContext: Context | null = null;

  // Kayıtlı durum bir kez, asenkron olarak okunur. Yüklenmeden önce yapılan çağrılar burada sırayla bekler.
  private loading: Promise<void> | null = null;
  private isLoaded = false;
  private pending: Array<() => void> = [];
  private writes: Promise<void> = Promise.resolve();
  private queueWriteScheduled = false;

  /**
   * Açılışta bir kez çağırın. `environment` yalnızca release build'ler içindir: uygulamanız hangisi olduğunu
   * zaten biliyorsa "testflight" veya "app_store" verin. Geliştirme build'leri (`__DEV__`) her zaman "debug" olur.
   */
  configure(writeKey: string, options: { environment?: EventEnvironment } = {}): void {
    if (!SUPPORTED) return;
    this.writeKey = writeKey;
    if (!__DEV__ && options.environment) this.environment = options.environment;
    void this.load();
    if (this.timer !== null) return;
    this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
  }

  /**
   * Bu install'ın kimliği; ilk izlenen event'ten önce ve izleme kapalıyken null.
   * Kullanıcının verilerinin silinmesini isteyebilmesi için bir destek ekranında gösterin.
   */
  async currentAnonymousId(): Promise<string | null> {
    if (!SUPPORTED) return null;
    await this.load();
    return this.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.
   */
  setEnabled(enabled: boolean): void {
    this.whenLoaded(() => {
      this.isEnabled = enabled;
      this.write(() => (enabled ? AsyncStorage.removeItem(DISABLED_KEY) : AsyncStorage.setItem(DISABLED_KEY, "1")));
      if (enabled) return;
      this.anonymousId = null;
      this.userId = null;
      this.queue = [];
      this.write(() => AsyncStorage.removeItem(ANONYMOUS_ID_KEY));
      this.write(() => AsyncStorage.removeItem(USER_ID_KEY));
      this.write(() => AsyncStorage.removeItem(QUEUE_KEY));
    });
  }

  /** Bir event'i kaydeder. Hafiftir; ağ için asla beklemez. İzleme kapalıyken hiçbir şey yapmaz. */
  track(name: string, properties: EventProperties = {}): void {
    if (!SUPPORTED) return;
    // Kimlik ve zaman, kayıtlı durum hâlâ yükleniyor olsa bile şimdi sabitlenir.
    const eventId = uuidV4();
    const occurredAt = new Date().toISOString();
    const props = { ...properties };
    this.whenLoaded(() => {
      if (!this.isEnabled) return;
      const anonymousId = this.anonymousId ?? this.newAnonymousId();
      this.queue.push({
        event_id: eventId,
        name,
        anonymous_id: anonymousId,
        user_id: this.userId,
        occurred_at: occurredAt,
        properties: props,
        context: this.context(),
      });
      if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE);
      this.persistQueue();
      if (this.queue.length >= FLUSH_AT) void this.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. */
  identify(userId: string): void {
    this.whenLoaded(() => {
      if (!this.isEnabled) return;
      this.userId = userId;
      this.write(() => AsyncStorage.setItem(USER_ID_KEY, userId));
    });
  }

  /** Oturum kapatılırken çağırın: sonraki event'ler yeni bir anonim kişiye ait olur. */
  reset(): void {
    this.whenLoaded(() => {
      this.userId = null;
      this.anonymousId = null;
      this.write(() => AsyncStorage.removeItem(USER_ID_KEY));
      this.write(() => AsyncStorage.removeItem(ANONYMOUS_ID_KEY));
    });
  }

  /** Kuyruktaki event'leri gönderir. Bir yeniden deneme beklemesi sürerken hiçbir şey yapmaz. */
  async flush(): Promise<void> {
    if (!SUPPORTED) return;
    await this.load();
    const writeKey = this.writeKey;
    if (!this.isEnabled || this.isFlushing || writeKey === null || this.queue.length === 0) return;
    if (this.retryAt > Date.now()) return;
    this.isFlushing = true;
    try {
      while (this.queue.length > 0) {
        const batch = this.queue.slice(0, MAX_BATCH);
        const environment = this.environment;
        const events = batch.map((event) =>
          event.context.environment !== undefined || environment === undefined
            ? event
            : { ...event, context: { ...event.context, environment } },
        );

        let response: Response;
        const abort = new AbortController();
        const timeout = setTimeout(() => abort.abort(), REQUEST_TIMEOUT_MS);
        try {
          response = await fetch(ENDPOINT, {
            method: "POST",
            headers: { "Content-Type": "application/json", "X-Write-Key": writeKey },
            body: JSON.stringify({ events }),
            signal: abort.signal,
          });
        } catch {
          this.delayRetry(null); // çevrimdışı: kuyruğu tut
          return;
        } finally {
          clearTimeout(timeout);
        }
        if (!this.isEnabled) return; // gönderim sırasında kapatıldı: kuyruk zaten silindi

        const status = response.status;
        if (status === 429 || status >= 500) {
          const header = response.headers.get("Retry-After");
          const retryAfter = header === null ? Number.NaN : Number(header);
          this.delayRetry(status === 429 && retryAfter >= 0 ? retryAfter : null); // kuyruğu tut
          return;
        }
        this.retryAt = 0;
        this.backoffSeconds = 0;
        if (__DEV__) await report(status, response);
        // 2xx: saklandı. Diğer 4xx: aynı paketi yeniden denemek başarılı olamaz.
        const sent = new Set(batch.map((event) => event.event_id));
        this.queue = this.queue.filter((event) => !sent.has(event.event_id));
        this.persistQueue();
      }
    } finally {
      this.isFlushing = false;
    }
  }

  /** Sunucu belirtirse `seconds` kadar, aksi hâlde 30 sn, 60 sn, 120 sn… en fazla 10 dakika bekler. */
  private delayRetry(seconds: number | null): void {
    this.backoffSeconds = Math.min(Math.max(this.backoffSeconds * 2, 30), 600);
    this.retryAt = Date.now() + (seconds ?? this.backoffSeconds) * 1000;
  }

  private newAnonymousId(): string {
    const id = uuidV4();
    this.anonymousId = id;
    this.write(() => AsyncStorage.setItem(ANONYMOUS_ID_KEY, id));
    return id;
  }

  private context(): Context {
    this.cachedContext ??= {
      app_version: DeviceInfo.getVersion(),
      build: DeviceInfo.getBuildNumber(),
      os_version: DeviceInfo.getSystemVersion(),
      device_model: DeviceInfo.getDeviceId(), // donanım tanımlayıcısı, ör. "iPhone16,2"
      locale: deviceLocale(),
    };
    return this.cachedContext;
  }

  private whenLoaded(operation: () => void): void {
    if (!SUPPORTED) return;
    if (this.isLoaded) {
      operation();
      return;
    }
    this.pending.push(operation);
    void this.load();
  }

  private load(): Promise<void> {
    this.loading ??= this.readStoredState();
    return this.loading;
  }

  private async readStoredState(): Promise<void> {
    try {
      const [disabled, anonymousId, userId, queue] = await Promise.all([
        AsyncStorage.getItem(DISABLED_KEY),
        AsyncStorage.getItem(ANONYMOUS_ID_KEY),
        AsyncStorage.getItem(USER_ID_KEY),
        AsyncStorage.getItem(QUEUE_KEY),
      ]);
      this.isEnabled = disabled === null;
      this.anonymousId = anonymousId; // ilk izlenen event'le oluşturulur
      this.userId = userId;
      const stored: unknown = queue === null ? [] : JSON.parse(queue);
      if (Array.isArray(stored)) this.queue = stored as Event[];
    } catch {
      // Depolama okunamıyor: boş bir kuyrukla başla.
    }
    this.isLoaded = true;
    // Yükleme sırasında yapılan çağrılar şimdi sırayla çalışır; event'leri kayıtlı olanların arkasına eklenir.
    const pending = this.pending;
    this.pending = [];
    for (const operation of pending) operation();
  }

  /** Kuyruğu her değişiklik grubu için bir kez, zaten bekleyen yazmalardan sonra kaydeder. */
  private persistQueue(): void {
    if (this.queueWriteScheduled) return;
    this.queueWriteScheduled = true;
    this.write(() => {
      this.queueWriteScheduled = false;
      return AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(this.queue));
    });
  }

  /** Depolama yazmaları art arda çalışır, böylece yapıldıkları sırayla gerçekleşir. */
  private write(operation: () => Promise<void>): void {
    this.writes = this.writes.then(operation).catch(() => undefined);
  }
}

/** Yalnızca geliştirme build'lerinde: atılan paketleri ve reddedilen veya sınırlanan event'leri konsola yazdırır. */
async function report(status: number, response: Response): Promise<void> {
  let json: Record<string, unknown> = {};
  try {
    const parsed: unknown = await response.json();
    if (parsed !== null && typeof parsed === "object") json = parsed as Record<string, unknown>;
  } catch {
    // JSON değil: yalnızca durum kodunu yaz.
  }
  if (status < 200 || status >= 300) {
    console.log(`[EventTracker] HTTP ${status}, batch dropped:`, json);
    return;
  }
  for (const key of ["rejected", "clamped"]) {
    const items = json[key];
    if (Array.isArray(items) && items.length > 0) console.log(`[EventTracker] ${key}:`, items);
  }
}

/** Rastgele bir UUID v4. Hermes'te crypto.randomUUID yoktur, bu yüzden rastgele baytlardan oluşturulur. */
function uuidV4(): string {
  const bytes = new Uint8Array(16);
  const crypto = (globalThis as { crypto?: { getRandomValues?: (array: Uint8Array) => Uint8Array } }).crypto;
  if (crypto?.getRandomValues) {
    crypto.getRandomValues(bytes);
  } else {
    for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
  }
  let nibble = 0;
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
    const byte = bytes[nibble >> 1] ?? 0;
    const value = nibble % 2 === 0 ? byte >> 4 : byte & 0x0f;
    nibble++;
    return (char === "x" ? value : (value & 0x3) | 0x8).toString(16);
  });
}

/** `language_REGION` biçiminde device dili, ör. "en_US" (bölge yoksa yalnızca "en"). */
function deviceLocale(): string {
  try {
    const tag = Intl.DateTimeFormat().resolvedOptions().locale; // ör. "en-US", "zh-Hans-CN"
    const [language = "", ...subtags] = (tag.split("-u-")[0] ?? "").split("-");
    const region = subtags.find((part) => /^([A-Z]{2}|\d{3})$/.test(part));
    return region ? `${language}_${region}` : language;
  } catch {
    return "";
  }
}

export const EventTracker = new Tracker();

5.1 Uygulamaya bağlama

Tracker'ı yapılandırın ve soğuk app_opened event'ini her açılışta bir kez, uygulamanın kök dosyasının modül kapsamında gönderin; böylece ikisi de yeniden render'da veya StrictMode altında iki kez çalışmaz. Bare React Native: App.tsx. Expo Router: app/_layout.tsx.

TypeScript
import { useEffect } from "react";
import { AppState } from "react-native";
import { EventTracker } from "./eventTracker";

// Modül kapsamı: her açılışta bir kez çalışır.
EventTracker.configure("YOUR_WRITE_KEY");
EventTracker.track("app_opened", { source: "cold" });

export default function App() {
  useEffect(() => {
    let wasInBackground = false;
    const subscription = AppState.addEventListener("change", (state) => {
      if (state === "background") {
        wasInBackground = true;
        void EventTracker.flush();
      } else if (state === "active" && wasInBackground) {
        wasInBackground = false;
        EventTracker.track("app_opened", { source: "warm" });
      }
    });
    return () => subscription.remove();
  }, []);

  return <RootNavigator />;
}

app_opened her active durumunda değil, yalnızca arka plandan dönüşte gönderilir: sistem uyarıları, Denetim Merkezi ve Face ID, uygulama hiç arka plana geçmeden onu inactive ve ardından yeniden active yapar.

Geliştirme sırasında Fast Refresh, o dosyayı düzenledikten sonra modül kapsamını yeniden çalıştırabilir ve bu da fazladan bir soğuk app_opened gönderir. Bu event'ler debug olarak işaretlenir ve varsayılan görünümün dışında kalır.

TestFlight'ı App Store'dan ayırt etme. Uygulamanın build yapılandırması bir release build'in hangi kanal için olduğunu zaten biliyorsa (örneğin EAS build profili veya Xcode scheme başına ayarlanan bir ortam değişkeni), bunu configure'a verin. Geliştirme build'leri bunu yok sayar ve debug olarak kalır.

TypeScript
EventTracker.configure("YOUR_WRITE_KEY", {
  environment: IS_TESTFLIGHT_BUILD ? "testflight" : "app_store",
});

IS_TESTFLIGHT_BUILD, uygulamanın yapılandırması ne sağlıyorsa onun yerine geçer. Böyle bir değer yoksa environment alanını vermeyin.

5.2 Event takibi

TypeScript
import { EventTracker } from "./eventTracker";

EventTracker.track("paywall_viewed", { placement: "onboarding" });
EventTracker.track("onboarding_completed", { duration_s: 42 });

// Uygulamanızda oturum açma / kapatmadan sonra:
EventTracker.identify(user.id);
EventTracker.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.
EventTracker.setEnabled(userAllowsAnalytics);

// Destek ekranı: kullanıcının verilerinin silinmesini isteyebilmesi için bunu gösterin (henüz hiçbir şey izlenmediyse null).
const installId = await EventTracker.currentAnonymousId();

track, identify, reset ve setEnabled hemen döner ve render handler'ları ile buton callback'leri dahil her koddan çağrılabilir. flush ve currentAnonymousId promise döndürür.

İzleme onayı bekliyorsa setEnabled fonksiyonunu §5.1 içindeki açılış app_opened event'inden önce çağırın; izleme kapalıyken o çağrı hiçbir şey yapmaz ve hiçbir kimlik oluşturulmaz. Seçim asenkron olarak saklanıyorsa yine de önce modül kapsamında setEnabled(false) çağırın, kayıtlı seçim evet dediğinde de setEnabled(true) çağırın: çağrılar yapıldıkları sırayla etkili olur.

Özellik değerlerinin tipi EventValue'dur: string | number | boolean | null. TypeScript nesneleri ve dizileri reddeder.

Satın almalar. Satın almaları satın alma akışında, kullanıcının satın al düğmesine dokunduğu yerde, uygulamanın kullandığı kütüphanenin (react-native-iap, RevenueCat veya başka bir kütüphane) satın alma çağrısının etrafında izleyin. Geri yüklemeleri, yenilemeleri veya başka device'lardan yapılan satın almaları da ileten bir listener ya da callback içinde izlemeyin. Aşağıdaki sarmalayıcı, uygulamanın satın alma çağrısını dört sonuçtan birini bildiren bir fonksiyon olarak alır.

TypeScript
import { EventTracker } from "./eventTracker";

export type PurchaseOutcome = "success" | "cancelled" | "pending" | "error";

/**
 * `purchase`, uygulamanın kendi satın alma çağrısıdır ve bir sonuca eşlenir:
 * tamamlandı ve doğrulandı → "success"; kullanıcı iptal etti → "cancelled";
 * Ask to Buy veya başka bir ertelenmiş onay → "pending"; diğer her şey → "error" (veya hata fırlatın).
 */
export async function buy(
  product: { id: string; price: number; currency: string },
  startsTrial: boolean,
  purchase: () => Promise<PurchaseOutcome>,
): Promise<PurchaseOutcome> {
  const base = { product_id: product.id };
  EventTracker.track("purchase_started", base);
  let outcome: PurchaseOutcome;
  try {
    outcome = await purchase();
  } catch {
    outcome = "error";
  }
  switch (outcome) {
    case "success":
      EventTracker.track("purchase_completed", {
        ...base,
        price: product.price,
        currency: product.currency,
        trial: startsTrial,
      });
      if (startsTrial) EventTracker.track("trial_started", base);
      break;
    case "cancelled":
      EventTracker.track("purchase_failed", { ...base, reason: "cancelled" });
      break;
    case "error":
      EventTracker.track("purchase_failed", { ...base, reason: "failed" });
      break;
    case "pending":
      break; // Ask to Buy veya ek onay: henüz tamamlanmadı, hiçbir şey izlemeyin
  }
  return outcome;
}

Bunu satın al düğmesinden, paywall'un gösterdiği ürünle ve kütüphanenin satın alma metodunu çalıştırıp sonucunu eşleyen bir fonksiyonla çağırın:

TypeScript
const outcome = await buy(product, startsTrial, async () => {
  const result = await yourPurchaseCall(product.id); // kütüphanenin satın alma metodu
  return toPurchaseOutcome(result); // "success" | "cancelled" | "pending" | "error"
});

product.price sayı olarak fiyat, product.currency ise ISO 4217 para birimi kodudur ("USD"); ikisi de kütüphanenin ürün nesnesinden gelir. startsTrial, paywall'unuzun zaten bildiği değerdir: bu satın alma ücretsiz trial başlatıyorsa true. Birçok kütüphane kullanıcının iptalini, iptal bayrağı veya hata kodu taşıyan fırlatılmış bir hata olarak bildirir; bunu "error" değil "cancelled" olarak eşleyin. Kütüphane sonucu yalnızca bir satın alma listener'ı üzerinden bildiriyorsa purchase_completed event'ini yalnızca kullanıcının bu akışta az önce başlattığı satın alma için izleyin.

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" ile paywall_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_Viewed ve paywall_viewed iki 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 veya reset() çağrılana kadar o install için AsyncStorage'da tuttuğu rastgele bir UUID. IDFA veya diğer uygulamalarla paylaşılan başka bir tanımlayıcı değildir.
  • user_id, yalnızca identify çağırırsanız.
  • §4.3 içindeki device bağlamı: uygulama version'ı, build, işletim sistemi sürümü, device modeli, dil ayarı ve biliniyorsa build ortamı.
  • İsteğin ağ konumundan çıkarılan ülke (bkz. §7.3).

Yalnızca iOS install'ları veri gönderir: Android'de referans kod hiçbir şey göndermez.

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_id değeri ve/veya anonymous_id değeri. Bir user_id, ona bağlı tüm anonymous_id değerlerini de kapsar. Hesabı olmayan kullanıcılar için uygulamanız await EventTracker.currentAnonymousId() ile alınan anonymous_id değerini, örneğin bir destek ekranında gösterebilir (§5.2). Bu değeri setEnabled(false) veya reset() çağırmadan önce okuyun: ikisi de onu 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.setEnabled(false) çağırın ve kullanıcı kabul edene kadar böyle bırakın, ardından EventTracker.setEnabled(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_id değerini yalnızca izleme açıkken izlenen ilk event'le oluşturur. Kullanıcı daha sonra onayını geri çekerse setEnabled(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, önce currentAnonymousId() değ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_id gönderiyorsanız yukarıdaki her satırı Linked to You olarak beyan edin: event'ler sisteminizdeki bir hesap kimliğiyle ilişkilendirilir. Hiç user_id gö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 JavaScript'tir ve Apple'ın gerekçe istediği hiçbir API'yi kendisi çağırmaz, ancak iki paketin native kodu çağırır ve her paket bunu beyan eden kendi PrivacyInfo.xcprivacy dosyasıyla gelir. Bu rehber yazıldığı sırada güncel olan sürümlerde @react-native-async-storage/async-storage dosya zaman damgasını (NSPrivacyAccessedAPICategoryFileTimestamp, C617.1), react-native-device-info ise UserDefaults (CA92.1), dosya zaman damgası (C617.1), sistem açılış zamanı (NSPrivacyAccessedAPICategorySystemBootTime, 35F9.1) ve disk alanını (NSPrivacyAccessedAPICategoryDiskSpace, 85F4.1) beyan eder. Sizin yine de kontrol etmeniz gerekenler:
    • Uygulamayı Xcode'da arşivleyin ve gizlilik raporunu oluşturun (Organizer → arşive sağ tıklayın → Generate Privacy Report). Rapor bu gerekçeleri listelemelidir. Listelemiyorsa onları uygulamanın kendi manifest'ine ekleyin: bare React Native'de ios/<YourApp>/PrivacyInfo.xcprivacy (React Native şablonu bunu oluşturur); Expo'da uygulama yapılandırmasındaki ios.privacyManifests, ardından yeniden prebuild.
    • Uygulamanın kendi manifest'indeki toplanan veri tipleri (NSPrivacyCollectedDataTypes) etiketinizle eşleşmelidir. Paketler hiçbirini beyan etmez, çünkü event toplamak sizin uygulamanızın onlarla yaptığı iştir.
    • Paketlerden birini yükselttikten sonra PrivacyInfo.xcprivacy dosyasına yeniden bakın: beyan edilen gerekçeler değişebilir.

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

  1. iOS uygulamasını çalıştırın (simülatör veya device, debug build), birkaç event tetikleyin, ardından uygulamayı arka plana gönderin (bu, kuyruğu gönderir).
  2. Dashboard → uygulamanız → Events → Canlı: event'leriniz saniyeler içinde görünür.
  3. 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, test build'leri ve App Store build'leri tarafından aynı şekilde kullanılan tek bir yazma anahtarı vardır. Referans kod, geliştirme build'inden (__DEV__) gelen her event'i debug olarak işaretler. TestFlight build'leri dahil release build'ler, configure'a verilen environment değerini taşır (§5.1); bu değer verilmezse hiçbir ortam bilgisi taşımaz ve App Store event'i olarak sayılırlar, bu durumda TestFlight çalıştırmaları varsayılan görünümde görünür. Dashboard varsayılan olarak app_store event'lerini ve ortam bilgisi olmayan event'leri gösterir; geliştirme çalıştırmalarınızı görmek için ortam filtresini Tüm build'ler olarak değiştirin. 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 bulun.

Belirti Olası neden
Canlı'da hiçbir şey yok Yanlış anahtar (401); configure hiç çağrılmadı; uygulama Android'de çalıştırıldı (tracker orada hiçbir şey yapmaz); 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. Geliştirme build'inde Metro / JS 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
Uygulama açılışta AsyncStorage veya DeviceInfo kaynaklı bir native modül hatasıyla çöküyor (modül null veya bulunamadı) Paketler kurulduktan sonra iOS uygulaması yeniden derlenmemiş: npx pod-install çalıştırıp yeniden derleyin veya yeni bir Expo development build oluşturun. Expo Go react-native-device-info paketini çalıştıramaz
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)
Geliştirmede fazladan soğuk app_opened Fast Refresh modül kapsamını yeniden çalıştırdı (§5.1); geliştirme event'leri debug olarak işaretlenir

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.

  • @react-native-async-storage/async-storage ve react-native-device-info kurulu (pod'lar kurulmuş veya yeni bir Expo development build oluşturulmuş) ve başka hiçbir bağımlılık eklenmemiş.
  • eventTracker.ts uygulamanın kaynak kodunda, §5'teki hâliyle değiştirilmeden duruyor ve iOS uygulaması derlenip açılıyor.
  • configure(...) açılışta bir kez, modül kapsamında, bu uygulamanın gerçek anahtarıyla çalışıyor; YOUR_WRITE_KEY metni projede hiçbir yerde geçmiyor.
  • EventTracker.flush(), AppState background durumuna geçtiğinde çalışıyor.
  • app_opened açılışta (source: "cold") ve arka plandan her dönüşte (source: "warm") izleniyor; her active durumunda değil.
  • §6'daki event'lerden akışı bu uygulamada bulunan her biri, listelenen özelliklerle izleniyor. Her birini event → file:line olarak raporlayın ve atladığınız §6 event'lerini nedenleriyle listeleyin.
  • Satın alma event'leri, geri yüklemeleri veya yenilemeleri de ileten bir listener içinde değil, satın alma akışında, satın alma çağrısının etrafı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: identify oturum açıldıktan sonra uygulamanın kendi kullanıcı kimliğiyle, reset ise oturum kapatıldıktan sonra çağrılıyor. Hesap yoksa ikisi de çağrılmıyor.
  • Canlı API: §3'teki curl testi accepted: 1 döndürüyor ve iOS uygulamasını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, uygulamanın gizlilik raporundaki gerekçe gerektiren API kayıtlarına (§7.7), uygulamanın gizlilik politikasına ve varsa onay gerekliliğine göre kontrol etmesi için geliştiriciye verildi. Uygulama Kids kategorisinde değil.