Kullanıcı bildirimleri kapattı: bildirim sistemi nasıl tasarlanır?
Bildirimleri kapatan kullanıcı çoğu zaman bildirimden değil, ayrım yapılmamasından rahatsızdır. Siparişi kargoya verildiğinde haber almak isteyen kişi aynı kanaldan kampanya duyurusu da alıyorsa tek bir anahtarı kapatır ve siz ikisini birden kaybedersiniz. İşleyen kurgunun özeti kısa: olayı bildirimden ayırın, her bildirimi bir kategoriye bağlayın, tercihleri kategori ve kanal kesişimi olarak tutun, aciliyeti işletim sisteminin anladığı biçimde beyan edin, gönderimi istek hattının dışına çıkarın. Aşağıdaki başlıklar bu beşinin pratikte nerede bozulduğunu anlatıyor.
Olay bir şeydir, bildirim başka bir şey
Çalışan bir bildirim altyapısında üç ayrı katman vardır. En altta iş olayı durur: "sipariş kargoya verildi". Ortada bildirim kararı: bu olaydan kim haberdar edilecek, hangi kategoriye giriyor, hangi kanaldan ve ne zaman gidecek. En üstte teslim: APNs, FCM, SMTP, SMS sağlayıcısı, uygulama içi liste.
Çoğu kod tabanında orta katman yoktur. Kargo servisi doğrudan sendOrderShippedEmail() çağırır. Bu satır çalıştığı sürece sorun görünmez. Sorun ikinci kanalı eklediğinizde çıkar: artık aynı yere bir push çağrısı daha koymanız, kullanıcının tercihini orada kontrol etmeniz, iki kanalın birbirini tekrarlamasını orada engellemeniz gerekir. Üçüncü kanalda bu kontroller üç ayrı yerde yaşamaya başlar ve biri mutlaka güncellenmeden kalır.
Orta katmanı ayırmanın bedeli bir tablo ve bir servistir. Olay yayınlanır, bildirim servisi alıcıları ve kanalları hesaplar, her teslim için bir kayıt yazar, kuyruğa bırakır. Kargo kodu kime ne gittiğini bilmez. Yeni kanal eklemek tek bir yerde iş olur.
Hangi olay bildirim hak eder?
Üç soruyla eleyin. Kullanıcının bir şey yapmasını gerektiriyor mu? Kullanıcının sahibi olduğu ya da takip ettiği bir şeyle mi ilgili? Gecikirse zarar var mı? Üçüne de hayır diyorsanız o kayıt uygulama içi listede durmalı, cebinde titrememeli.
Atlanan bir kural daha var: kullanıcıya kendi yaptığı işi bildirmeyin. Bir kaydı kendisi güncelleyen kişi "kayıt güncellendi" bildirimi almamalı. Bu tek filtre, çok kullanıcılı B2B uygulamalarında bildirim hacmini gözle görülür biçimde düşürür ve yazması bir saat sürer. Aynı şekilde, kullanıcı zaten o ekranda açıkken o kaydın bildirimini push olarak göndermenin anlamı yoktur.
Varsayılan olarak açık gelecek kategorileri dar tutun: güvenlik uyarıları, hesapla ilgili zorunlu bilgilendirmeler, kullanıcıdan eylem bekleyen işler. Geri kalan her şey kullanıcının açtığı şey olsun. Kapatılamayan ve üzerine tıklanacak bir eylemi olmayan bildirim, sırada bekleyen bir şikâyettir.
Tercih merkezi bir kategori ve kanal matrisidir
Doğru arayüz bir ızgaradır: satırlarda kategoriler, sütunlarda kanallar, her hücrede bir anahtar. Kategori sayısını altı ile on arasında tutun. Otuz kategorili bir ekranda kullanıcı aradığını bulamaz, en üstteki "tümünü kapat"a basar.
Bazı hücreler kilitli olmalı: parola sıfırlama, güvenlik uyarısı, fatura. Bunları ekranda gizlemek yerine kilitli ve gerekçesi yazılı gösterin, çünkü gizlenen tercih kullanıcıya sistemin tercihlerine uymadığını düşündürür.
Veri tarafında küçük bir karar çok iş kazandırır: her kullanıcı için tüm matrisi satır satır yazmayın. Varsayılanları kodda tutun, veritabanına yalnızca kullanıcının varsayılandan saptığı hücreleri yazın. Böylece yeni bir kategori eklediğinizde milyonlarca satırlık geri doldurma işi çıkmaz, varsayılanı değiştirdiğinizde de tercihini açıkça belirtmemiş herkes yeni varsayılana geçer.
Aciliyet, işletim sisteminin bildiği bir özelliktir
Bildirimin ne kadar rahatsız edeceğine siz karar vermezsiniz, kullanıcı ve işletim sistemi birlikte karar verir. iOS 15'ten beri her bildirimin bir kesinti seviyesi vardır: passive sessizce listeye düşer, active varsayılan davranıştır, timeSensitive odak modlarını deler, critical sessiz moda kadar her şeyi deler. Son ikisi Apple'dan ayrıca yetki (entitlement) almayı gerektirir ve her uygulamaya verilmez. Bunu bilmeyen ekipler "önemli" bildirimlerini normal seviyeden gönderip odak modunda kaybolmasına şaşırır.
Android tarafında karşılığı bildirim kanallarıdır. Android 8.0'dan (API 26) beri her bildirim bir kanala bağlanmak zorundadır ve kullanıcı ses, titreşim, önem seviyesi gibi ayarları kanal bazında kendisi değiştirebilir. Burada kalıcı bir tuzak var: kanal bir kez oluşturulduktan sonra uygulama onun davranışını programla değiştiremez, yalnızca adını ve açıklamasını güncelleyebilir. Yani ilk sürümde her şeyi "Genel" adlı tek bir kanaldan gönderdiyseniz, o karar kullanıcı uygulamayı silip yeniden kurana kadar sizinle kalır.
Pratik sonuç: kanal ve kategori listesini ilk sürümden önce oturup yazın. İlk sürüm kapsamını belirlerken sonradan düzeltmesi ucuz olan ve pahalı olan kararları ayırmanız gerekiyor; bildirim kanalları pahalı tarafta.
İzni ne zaman isteyeceğiniz, kaç kişinin açık bırakacağını belirler
Android 13 (API 33) ile bildirim göndermek POST_NOTIFICATIONS çalışma zamanı izni gerektiriyor ve yeni kurulumlarda bildirimler varsayılan olarak kapalı geliyor. Daha önce Android tarafında böyle bir engel yoktu, dolayısıyla eski uygulamalarınızın açık oranı yeni kurulumlarda aynı kalmaz.
iOS'ta sistem istemini hiç göstermeden başlamanın bir yolu var: geçici yetkilendirme (provisional authorization) ile bildirimler sessizce Bildirim Merkezi'ne düşer, kullanıcı ilkini gördüğünde "Tut" ya da "Kapat" der. Düşük riskli, yüksek hacimli kategoriler için izni önden istemekten daha iyi sonuç verir.
Web tarafında Push API bir service worker ve VAPID anahtar çifti ister. Safari'de durum ayrı: iOS ve iPadOS 16.4'ten beri web push destekleniyor, ancak yalnızca ana ekrana eklenmiş web uygulamaları için ve izin istemi doğrudan bir kullanıcı etkileşimine bağlı olmak zorunda. Sayfa açılır açılmaz izin isteyen kod tarayıcıda da zaten en kötü sonucu verir, çünkü reddedilen izni geri almak kullanıcıyı tarayıcı ayarlarına göndermeyi gerektirir.
Tek kural: izni, kullanıcının o bildirimi isteyeceği bir şey yaptığı anda isteyin. Sipariş verdikten sonra "kargo durumu için bildirim açalım mı" diye sormak, uygulama ilk açılışta sormaktan farklı bir cevap alır.
Aynı olay için üç bildirim gitmemeli
Kuyruklar en az bir kez teslim eder, yani arka plan işleri tarafındaki her yeniden deneme bir bildirimi iki kez gönderme ihtimali taşır. Çözüm teslim kaydına tekillik koymaktır: (kullanıcı, olay kimliği, kategori, kanal) dörtlüsü için benzersiz bir anahtar. Aynı dörtlü ikinci kez gelirse yazma başarısız olur ve gönderim atlanır.
Bir de tekrar değil, eskime sorunu var. Sipariş durumu on dakika içinde üç kez değişirse kullanıcının cebinde üç bildirim birikir ve ilk ikisi artık yanlıştır. Push sağlayıcıları bunun için çökertme kimliği sunar: APNs'te apns-collapse-id başlığı (en fazla 64 bayt), FCM'de collapse_key. Aynı kimlikle gelen yeni bildirim, henüz teslim edilmemiş olanın yerine geçer. Sipariş bildirimleri için bu kimliği sipariş numarasından türetmek çoğu durumda yeterli.
Özet (digest) basit bir zamanlayıcı değil
Toplu özette iki ayrı karar var ve bunları karıştırmak katı bir sisteme yol açar. Birincisi biriktirme penceresi: ne kadar bekleyeceksiniz. İkincisi sunum: biriken olayları tek bir özet mesajı olarak mı göndereceksiniz, yoksa tek tek ama gecikmeli mi.
Üç pencere tipi işe yarar. Sabit pencere kullanıcının yerel saatine göre belirli bir anda gönderir, günlük özet için uygundur. Kayan pencere son olaydan sonra belirli bir süre sessizlik bekler, yoğun aktivite bittiğinde tek mesaj gönderir. Eşikli yaklaşım ilkini anında geçirir, arkasından gelenleri biriktirir; sohbet ve yorum akışlarında en doğal duranı budur.
Özette sık atlanan bir kontrol var: göndermeden önce kullanıcının o kayıtları uygulama içinde okuyup okumadığına bakın. Okunmuş on maddeyi e-postayla tekrar sunan özet, kullanıcıya özetin kendisinin gereksiz olduğunu öğretir. Pencere kapandığında okunmamış kalan maddeyi gönderin, kalanını düşürün.
Saat dilimi kullanıcının, sizin değil
Her kullanıcı için saat dilimini açıkça saklayın. Sunucunun saati, hatta hesabın ülke alanı bu iş için yeterli değildir; tarih ve saat dilimi tarafındaki klasik hatalar burada doğrudan bildirim gönderme saatine yansır.
Sessiz saatler tanımlayın ve bir ayrıntıyı atlamayın: sessiz saat bittiğinde biriken her şeyi aynı anda boşaltmayın. Sabah 08:00'de arka arkaya düşen on bir bildirim, gece gelen on bir bildirimden daha kötüdür.
Her bildirimin bir son kullanma tarihi olmalı. Kullanıcı telefonunu iki gün kapalı tuttuysa dünkü "toplantı 15 dakika sonra" bildiriminin teslim edilmesi gereksiz. APNs apns-expiration, FCM ttl alanıyla bunu söylüyor; kendi kuyruğunuzda da aynı kontrolü yapın, çünkü bildirim sağlayıcıya ulaşmadan önce de bekliyor olabilir.
Uygulama içi bildirim merkezi: yazarken dağıtmak mı, okurken toplamak mı
İki tasarım var. Yazarken dağıtmak, olay gerçekleştiğinde her alıcı için bir satır yazmak demektir. Okurken toplamak ise olayı tek satır tutup kimin göreceğini sorgu anında hesaplamaktır.
Kurumsal uygulamaların çoğunda alıcı kümesi küçüktür ve yazarken dağıtmak doğru seçimdir. Okundu bilgisi, kullanıcıya özel sıralama ve okunmamış sayacı bedava gelir; tek maliyet satır sayısıdır. Yüz binlerce takipçili sosyal akışlar bunun tersini gerektirir, ama o problemi yaşamadan o mimariyi kurmayın.
Okunmamış sayacını da baştan düşünün. iOS'ta uygulama rozetindeki sayıyı sunucu gönderir, yani sunucunun o sayıyı biliyor olması gerekir. "Görüldü" ile "okundu" da aynı şey değildir: listeyi açmak görülmedir, maddeye tıklamak okunmadır. İkisini tek alanda birleştiren ekipler, kullanıcı listeye şöyle bir baktığı için tüm bildirimleri kaybeder.
"Gönderildi" teslim değil, teslim de okundu değil
APNs ya da FCM isteğinize 200 döndüğünde bildirim teslim edilmiş olmaz, sıraya alınmış olur. Cihaz kapalıysa, ağda değilse ya da kullanıcı izni geri aldıysa sonuç sessizce değişir. Gösterge panonuzda "gönderilen" ile "teslim edilen" ayrı iki sayı olmalı.
Jeton (token) temizliği en çok ihmal edilen iştir. Uygulama silindiğinde ya da jeton yenilendiğinde sağlayıcı size geçersiz jeton yanıtı döner; bu yanıtı yakalayıp kaydı silmeyen sistemler yıllar içinde ölü jeton yığını biriktirir ve kendi hata oranlarını okuyamaz hale gelir. Sağlayıcı arayüzlerinin de ömrü var: FCM'in eski HTTP ve XMPP uçları 20 Haziran 2023'te kullanımdan kaldırıldı, kapatma 22 Temmuz 2024'te başladı, bugün geçerli olan HTTP v1 arayüzü. Bu tür geçişleri sürüm değişikliklerini takip ettiğiniz yerde izleyin.
E-posta kanalında teslim meselesinin kendine ait bir dünyası var; kimlik doğrulama, şikâyet oranı ve tek tıkla abonelikten çıkma başlıkları işlemsel e-posta yazısında duruyor. Buradan tek not: bildirim sisteminizdeki kategori kapatma işlemi ile e-postadaki abonelikten çıkma aynı kayda yazmalı. İki ayrı yerde tutulan bu bilgi, er ya da geç kapattığı bildirimi almaya devam eden bir kullanıcı üretir.
Gönderimi istek hattından çıkarın, metni koda gömmeyin
Bildirim gönderimi kullanıcının beklediği istek içinde yapılmamalı. Sağlayıcı yavaşladığında bu, kaydet düğmesinin yavaşlaması demektir. Olayı veritabanına yazan işlemle aynı işlem içinde bir giden kutusu (outbox) satırı oluşturun, kuyruk onu alsın. Böylece işlem geri alındığında bildirim de gitmemiş olur.
Metinleri şablon kimliği ve veri olarak saklayın, hazır metin olarak değil. Dil seçimi gönderim anında kullanıcının tercihine göre çözülür. Bir de destek tarafının ihtiyacı var: "müşteriye tam olarak ne yazdık" sorusuna cevap verebilmek için gönderilen son hali de saklayın. Bu kayıt denetim izi tarafıyla aynı mantıkta çalışır, ama bildirim gövdesi kişisel veri taşıyabileceği için saklama süresini ayrıca belirleyin.
Test ortamından gerçek müşteriye bildirim gitmemeli
Bu hata sık ve utandırıcı. Veritabanı kopyasıyla ayağa kalkan bir test ortamı, içindeki gerçek adreslere ve gerçek cihaz jetonlarına bildirim göndermeye tamamen hazırdır. Üretim dışı ortamlarda tek bir çıkış kapısı olsun: ya her şeyi tek bir yakalama adresine yönlendirin ya da açık bir izin listesi dışındaki tüm gönderimleri düşürün. Sağlayıcıların sanal (sandbox) uçlarını kullanın. Ortam farklarını kapatırken bu kuralı kodun kendisine koyun, yapılandırma dosyasına değil; yanlış dosyanın kopyalanması her ekibin başına geliyor.
QA için bir önizleme ekranı yazmak yarım gün sürer ve her şablon değişikliğinde kazandırır: kategori ve kanal seçilir, örnek veriyle nasıl göründüğü ekranda çıkar, gerçek gönderim yapılmaz.
Ölçülecek dört sayı
Kullanıcı başına günlük bildirim sayısı. Bunu bir bütçe gibi görün, kategori eklendikçe sessizce büyür.
Kategori ve kanal kırılımında kapatma oranı. Hangi kategorinin kanalı yaktığını size bu sayı söyler; toplam kapatma oranı söylemez.
Gönderilen ve teslim edilen arasındaki fark, kanal bazında. Buradaki ani açılma genelde jeton ya da yapılandırma sorunudur, içerikle ilgisi yoktur.
Bildirimden gelen kullanıcıların işi tamamlama oranı. Açılma oranı tek başına yanıltır; bildirim kullanıcıyı doğru ekrana indirmiyorsa açılma yüksek, tamamlama düşük çıkar. Gösterge panonuzda bu dördü yan yana dursun.
Bu hafta yapılabilecek beş kontrol
- Kod tabanında bildirim gönderen tüm çağrıları listeleyin. Kaç ayrı yerden gönderiliyor, kaçı kullanıcı tercihini kontrol ediyor?
- Kategori listenizi yazın. Kullanıcı bunların hangisini tek tek kapatabiliyor, hangisini kapatmak için tüm kanalı kapatmak zorunda?
- Mobil uygulamanızda kaç bildirim kanalı tanımlı? Hepsi "Genel" adlı tek kanalda toplanıyorsa, bir sonraki sürüme kategori bazlı kanalları ekleyin.
- Aynı olay için iki bildirim gönderilmesini engelleyen bir tekillik kısıtı var mı? Yoksa yeniden denemede ne oluyor?
- Test ortamınızdan gerçek bir müşteri adresine bildirim gidebilir mi? Cevabı denemeden bilmiyorsanız cevap muhtemelen evet.
Bu maddelerin birinde net cevap çıkmıyorsa ya da kullanıcılarınız bildirimleri topluca kapatmaya başladıysa, mevcut gönderim akışını ve kategori yapısını birlikte çıkarıp somut bir düzeltme planı hazırlayabiliriz.
Bu konuda yardıma mı ihtiyacınız var?