Veri dışa / içe aktarma (export / import)
Faz 8 ile gelen export/import işleri maintenance kuyruğunda worker tarafından çalışır; dosyalar S3 uyumlu nesne depolamada (yerelde RustFS, S3_* ortam değişkenleri) tutulur. Dashboard: Kitle → Dışa / İçe Aktarım. API:…
Faz 8 ile gelen export/import işleri maintenance kuyruğunda worker tarafından çalışır; dosyalar S3 uyumlu nesne depolamada (yerelde RustFS, S3_* ortam değişkenleri) tutulur. Dashboard: Kitle → Dışa / İçe Aktarım. API: POST/GET /v1/apps/{app_id}/exports, POST/GET /v1/apps/{app_id}/imports (admin kapsamı).
Export
| Tür | Sütunlar |
|---|---|
users | id, external_id, tags (JSON), properties (JSON), session_count, first_seen_at, last_active_at, created_at — filters.segment_id ile segment kitlesi |
subscriptions | id, user_id, external_id, channel, status, token_masked (son 8), device (JSON), test_device, last_active_at, unsubscribed_at, created_at — filters.channel, filters.status |
events | id, user_id, subscription_id, name, properties (JSON), value, source, occurred_at — filters.name, from, to |
deliveries | id, message_id, campaign_id, subscription_id, user_id, channel, status, error_code, suppression_reason, sent_at, delivered_at, opened_at, clicked_at, created_at — filters.message_id, campaign_id, status, from, to |
- Dosya UTF-8 BOM ile CSV'dir (Excel uyumu);
=,+,-,@ile başlayan hücreler tek tırnakla etkisizleştirilir. - Üst sınır 1.000.000 satır; keyset sayfalama ile 5.000'lik gruplar okunur. Ölçüm: 100.000 kullanıcı ≈ 2 sn (18 MB).
GET …/exports/{id}tamamlanmış iş içindownload_urldöner (imzalı,EXPORT_URL_TTL_HOURS, varsayılan 24 saat). Tarayıcıya verilen adresS3_PUBLIC_ENDPOINTile belirlenir. Bu adres yol öneki içeremez (SigV4 imzası yolu ve Host'u kapsar); compose üretimde ayrı alan adı kullanılır:STORAGE_HOST(nginx yalnızca GET/HEAD iletir, Host korunur) veS3_PUBLIC_ENDPOINT=https://<STORAGE_HOST>.- Token'lar ham olarak dışa aktarılmaz (maskeli); e-posta/telefon gibi PII ileride eklenirse aynı maskeleme uygulanır.
Import
POST …/imports gövdesi multipart/form-data: type (csv_users | onesignal_players) ve file (CSV, en fazla 50 MB). İş tamamlanınca stats (processed, created_users, updated_users, created_subscriptions, skipped, errors) ve hatalı satırlar için error_report_url (sütunlar: line, error, row) döner.
Pushizo CSV (csv_users)
| Sütun | Açıklama |
|---|---|
external_id | Kullanıcı kimliği (abonelik yoksa zorunlu). Aynı external_id tekrar edince tag/property birleştirilir. |
channel | ios_push, android_push, email, sms (token varsa zorunlu; web_push CSV ile aktarılamaz: p256dh/auth anahtarları yok) |
token | Cihaz token'ı / e-posta / telefon; (app, channel, token) benzersiz, varsa güncellenir |
status | subscribed (varsayılan), unsubscribed, invalid |
language, timezone | properties.language ve device.timezone (IANA) |
tags | JSON nesnesi, ya da tag.<anahtar> sütunları |
properties | JSON nesnesi, ya da property.<anahtar> sütunları |
Örnek:
external_id,channel,token,language,timezone,tag.plan,tag.city
u-1001,ios_push,0a1b2c…,tr,Europe/Istanbul,pro,Ankara
u-1002,,,en,,free,
```text
### OneSignal player CSV (`onesignal_players`)
OneSignal panelindeki "Export" dosyası olduğu gibi yüklenir. Eşleme:
| OneSignal | Pushizo |
|-----------|------------|
| `external_user_id` | `external_id` (boşsa ve token varsa anonim kullanıcı) |
| `identifier` | token |
| `device_type` 0 → `ios_push`; 1, 2, 13 → `android_push`; 5, 7, 8, 9, 10 → web (yalnızca kullanıcı); 11 → `email`; 14 → `sms` |
| `invalid_identifier` = t | abonelik `invalid` |
| `notification_types` < 0 | abonelik `unsubscribed` |
| `tags` (JSON) | tags |
| `language` | `properties.language`, `device.locale` |
| `timezone` (UTC'den saniye) | `device.timezone` = `Etc/GMT∓h` (tam saat değilse boş) |
| `device_model`, `device_os`, `game_version` | `device.model`, `device.os_version`, `device.app_version` |
| `session_count`, `created_at`, `last_active` | `session_count`, `first_seen_at`, `last_active_at` |
Örnek dosya: `tests/fixtures/onesignal-players-sample.csv`. Import idempotenttir: aynı dosya ikinci kez yüklendiğinde kullanıcı/abonelik yinelenmez (`updated_users` artar).
## Sorun giderme
- İş `failed` ise `error` alanına bakın; depolama erişilemiyorsa API başlangıcında "Nesne depolama erişilemedi" uyarısı loglanır (`npm run infra:status` → rustfs).
- Büyük import dosyalarını 500 bin satırın altında tutun (`IMPORT_MAX_ROWS`); daha büyük dosyaları bölerek yükleyin.Postgres rol ayrımı (RLS'in üretimde etkin olması)
Kiracı izolasyonunun ikinci savunma hattı Postgres Row Level Security'dir (db/migrations/0005_rls.up.sql, FORCE ROW LEVEL SECURITY). Süper kullanıcılar ve BYPASSRLS rolleri RLS'e hiçbir koşulda tabi değildir; uygulama…
Runbook: Gerçek cihaza APNs ve FCM test gönderimi
Bu runbook, kanal kimlik bilgilerinin gerçek sağlayıcılarla çalıştığını doğrulamak için izlenir. Faz 3'te "doğrula" ucu sağlayıcıya test bağlantısı kurar; gerçek cihaza gönderim Faz 4 (POST /v1/notifications) ve Faz 5…