# Abrova docs (fa)
https://abrova.ir — backend services for app developers in Iran.
## ابرووا تریس
- SDK فلاتر: https://abrova.ir/docs/trace/flutter-sdk
enablePerformanceTracking() میسازد آدرس کامل درخواست را، همراه با query string، ثبت میکنند؛ پس اطلاعات محرمانه را در URL نگذارید یا آن hostها را با setPerformanceExcludedHosts(_:) مستثنا کنید. متد جداگانهی AbrovaTraceHttpBreadcrumbProtocol.enable() breadcrumbهای HTTP را با پنهان کردن پارامترهای حساس query (token، api_key، apikey، key، secret، password، pwd، auth، authorization، access_token، refresh_token، session) اضافه میکند و پاسخهای 5xx و درخواستهای ناموفق را بهصورت پیام گزارش میدهد.
AbrovaTrace.shared.close() را صدا میزند؛ صدا زدن آن توسط شما اختیاری است و لاگهای بافرشده را میفرستد.
| {h} | ))}||
|---|---|---|
| {endpoint} | {method} | {desc} |
سرعت بارگذاری را میسنجد. لحظهای را نشان میدهد که بزرگترین عنصر محتوا (تصویر، ویدیو یا بلوک متن) دیده میشود.
پاسخگویی کلی صفحه را میسنجد. همهی تعاملها را زیر نظر میگیرد و بدترین تأخیر را گزارش میکند.
پایداری بصری را میسنجد. مجموع همهی جابهجاییهای غیرمنتظرهی چیدمان در طول عمر صفحه است.
{name}
{desc}
خوب: {good}
| {h} | ))}|||
|---|---|---|---|
| {metric} | {weight} | {good} | {poor} |
| {h} | ))}|||
|---|---|---|---|
| {bucket} | {range} | {weight} | {meaning} |
| {h} | ))}||
|---|---|---|
| {sdk} | {platform} | {source} |
| {h} | ))}|
|---|---|
| {col} | {desc} |
{d}
{s.value}
{s.label}
Exception: FormatException: Invalid input
{" "}at Parser.parse (lib/parser.dart:42:15) {'←'} محل رخ دادن خطا
{" "}at DataService.process (lib/data_service.dart:108:8)
{" "}at HomeScreen.loadData (lib/screens/home.dart:55:12)
{" "}at _HomeScreenState.initState (lib/screens/home.dart:23:5)
{desc}
AbrovaTrace.initWithZone() یا AbrovaTrace.init()) نصب میشود و فراخوانی اضافهای لازم نیست. AbrovaTrace.close() فقط دادههای در انتظار را میفرستد و SDK را میبندد.
dart2js (کامپایلری که Dart را به جاوااسکریپت تبدیل میکند) در buildهای --release بهشدت inline میکند. بعضی frameها تمیز به فایلهای .dart شما برمیگردند. بعضی دیگر به بخشهای داخلی فریمورک (js_helper.dart، dart:async) میرسند یا minified میمانند (a3v.$0)، چون فایل .map برای آن بایتهای JS اصلاً نگاشتی ندارد.
این ویژگی خروجی dart2js است، نه مشکل resolver. از اول میگوییم چون بیشتر تیمها در production به آن برمیخورند و فکر میکنند تنظیماتشان خراب است. خراب نیست.
جایی که ابرووا جلو میافتد context است. breadcrumbهای gesture را فعال کنید (AbrovaTrace.enableGestureBreadcrumbs()) تا هر مشکل ویجتی را که کاربر درست پیش از خطا لمس کرده نشان دهد، بههمراه route، آخرین فراخوانی HTTP و هر breadcrumb سفارشی. همراه با trace نیمهبازگرداندهشده، گزارش باگی قابلبازتولید خواهید داشت.
فقط برای QA / staging میتوانید بهینهسازی کمتری را اجبار کنید تا traceهای کاملاً بازگرداندهشده بگیرید:
حجم bundle حدود ۳۰٪ بیشتر میشود؛ برای staging مناسب است، برای production توصیه نمیشود.
هر بار که build مربوط به production را با minification منتشر میکنید (Next.js، Vite، webpack، Rollup). بدون source mapهای آپلودشده، هر خطا در داشبورد ابرووا تریس stack فشردهشده را نشان میدهد که برای debug به کاری نمیآید.
«source mapها را آپلود کردم اما خطاها هنوز stack فشردهشده نشان میدهند.»
معمولاً رشتهی release در آپلود با چیزی که SDK میفرستد یکی نیست. نسخهی release را در صفحهی جزئیات خطا در داشبورد ابرووا تریس ببینید، سپس دقیقاً با همان رشته دوباره آپلود کنید.
{d}
{item.value}
{item.label}
| {h} | ))}||
|---|---|---|
| {level} | {count} |
نمونهی مسیر کاربر:
| {h} | ))}|
|---|---|
{type} |
{desc} |
{`{"status": "created", "id": 12345}`}
{`{"status": "created", "count": 3, "projectId": "..."}`}
| {h} | ))}||
|---|---|---|
| {type} | {desc} | {ex} |
{s.desc}
| {h} | ))}||
|---|---|---|
| حل کردن یک خطا | یک مشکل مشخص را رفع کردهاید | مشکل را باز کنید ← «حل شد» (میانبر R) |
| حل کردن همه | چند رفع با هم deploy شدهاند | مشکلها ← ⋯ ← «حل همه مشکلات باز…» |
موضوع: {'\u{1F4CA}'} گزارش هفتگی خطاهای شما - ۱۵ دسامبر
هفتهی ۱۵ دسامبر ۲۰۲۵
۸
خطای جدید
۱۲
حلشده
۵
باز
۴۷
رخداد
{'↑'} ۳۳٪ بیشتر از هفتهی قبل
پرتکرارترین خطاهای این هفته:
۱. TypeError: Cannot read 'map' of undefined — ۲۳ · باز
۲. NetworkError: Failed to fetch — ۱۵ · حلشده
۳. ReferenceError: user is not defined — ۹ · باز
موضوع: [AbrovaTrace] New Error: {'\u{1F6A8}'} Error Spike Detected: 500% increase in error rate
{'\u{1F6A8}'} Error Spike Detected: 500% increase in error rate
Project: My Flutter App
5
Occurrences (new issues, last hour)
~1
Baseline (avg/hour)
Error rate increased from ~1/hour to 5/hour
ab_live_...) هم تا وقتی تنظیم پروژهی server routeها به server secret نیاز دارند خاموش است پذیرفته میشود؛ اما این کلید داخل اپهای شما منتشر میشود، پس هر کسی آن را استخراج کند میتواند check-in جعلی «ok» بفرستد. وقتی کارهایتان از کلید سرور استفاده کردند، این تنظیم را روشن کنید. کلید سرور و routeهای سرور را ببینید.
| {h} | ))}|||
|---|---|---|---|
{field} |
{type} | {required === 'بله' ? بله : خیر} | {desc} |
{s.desc}
monitor_slug دقیقاً یکی استab_srv_ (یا ab_live_) شروع میشودserver_credential_required میگوید، تنظیم پروژهی server routeها به server secret نیاز دارند روشن است و کار کلید اپ را فرستاده؛ از کلید سرور استفاده کنیدX-API-Key فرستاده میشود{`{"success": true, "deploy": { ... }}`}
#### لاگهایی که از سرورها فرستاده میشوند
[Logs REST API](/docs/trace/logs-rest-api) هیچوقت بهخاطر این تنظیم رد نمیشود، اما سرور بهتر است همچنان با کلید سرور احراز هویت کند، نه با کلید اپ. SDKهای Node.js و Laravel همین حالا کلید سروری را که با آن پیکربندیشان کردهاید میفرستند.
کار هنوز کلید اپ را میفرستد. در هدر X-API-Key آن را با کلید سرور (ab_srv_...) عوض کنید، یا هدر x-server-secret را اضافه کنید. تا آن موقع مانیتور کار را اجرانشده نشان میدهد.
agent هنوز با کلید اپ اجرا میشود. خطوط لاگ همچنان میرسند، اما agent دیگر نمیتواند کانتینرها را ثبت کند، وضعیتشان را بفرستد یا دستور بگیرد. آن را با مقدار کلید سرور در ABROVA_TRACE_API_KEY دوباره deploy کنید.
آن را همهجا بهروز کنید: agent، cron jobها، اسکریپتهای deploy و پیکربندی SDK بکاند. کلید قدیمی همان لحظهی عوض کردن از کار میافتد.
email1@domain.com/revoke در BotFather توکن تازه بسازیدmyorg، نام abrova-tracegithub.com/myorg/abrova-trace✓ توصیهشده
```dart void main() async { await AbrovaTrace.initWithZone( AbrovaTraceConfig( apiKey: 'ab_live_your_api_key', environment: 'production', release: '1.0.0', captureUncaughtErrors: true, ), () async { WidgetsFlutterBinding.ensureInitialized(); runApp(MyApp()); }, ); } ```✗ ساده (خطاهای async را از دست میدهد)
```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // This only catches some errors await AbrovaTrace.init( AbrovaTraceConfig( apiKey: 'ab_live_your_api_key', environment: 'production', ), ); runApp(MyApp()); } ```{`dependencies:
abrova_auth:
hosted: https://center.abrova.ir/api/packages/abrova/pub
version: ^0.1.1`}
بعد `flutter pub get` را اجرا کنید و وارد شوید:
```dart
import 'package:abrova_auth/abrova_auth.dart';
final auth = await AbrovaAuth.initialize('ab_live_your_api_key');
auth.authStateChanges.listen((user) {
// null when signed out
});
if (auth.currentUser == null) {
await auth.signInAnonymously();
}
print(auth.currentUser?.id); // ru_...
```
کاربر از اولین اجرا یک شناسهی پایدار و یک توکن کاربر میگیرد. بعداً یک روش ورود را به همان کاربر وصل کنید — دادههایش مال خودش میماند:
```dart
await auth.linkPassword(email: 'sara@example.com', password: password);
```
روی وب هم همینطور است، بعد از اینکه scope `@abrova` را در `.npmrc` به Abrova Center ارجاع دادید:
{`@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/`}
```typescript
import { createAbrovaAuth } from '@abrova/auth-web';
const auth = createAbrovaAuth({ apiKey: 'ab_live_your_api_key' });
await auth.ready();
if (!auth.isSignedIn) await auth.signInAnonymously();
console.log(auth.currentUserId); // ru_...
```
صفحهی هر پلتفرم راهاندازی کامل را دارد: [Flutter](/docs/auth/flutter)، [iOS](/docs/auth/ios)، [Android](/docs/auth/android)، [Web](/docs/auth/web)، [React Native](/docs/auth/react-native).
{`dependencies:
abrova_auth:
hosted: https://center.abrova.ir/api/packages/abrova/pub
version: ^0.1.1`}
سپس نصبش کنید:
```bash
flutter pub get
```
در کنسول ابرووا، **Abrova Auth** را برای پروژه (**تنظیمات › محصولات**) و روشهای ورود دلخواه را (**Abrova Auth › روشهای ورود**) روشن کنید.
Abrova Auth را در تنظیمات › محصولات و روش را در Abrova Auth › روشهای ورود روشن کنید. با getMethods() فقط روشهایی را نشان دهید که روشناند.
پیش از خواندن currentUser، initialize() را صدا بزنید و فقط وقتی currentUser برابر null است وارد شوید.
بررسی کنید bundle ID، Services ID یا client ID در کنسول باشد، و به ارائهدهنده AbrovaAuthNonce.sha256Hex(raw) و به ابرووا مقدار خام را داده باشید.
آنها توکنشان را cache میکنند. متد refresh آنها (مثلاً AbrovaSync.refreshUserToken()) را از listener مربوط به authStateChanges صدا بزنید.
AbrovaAuth.shared.configure(apiKey:) را هنگام شروع اپ، پیش از هر فراخوانی دیگر، صدا بزنید.
bundle ID اپ را در Abrova Auth › روشهای ورود › Apple، و قابلیت Sign in with Apple را به target اضافه کنید.
Abrova Auth را در تنظیمات › محصولات و روش را در Abrova Auth › روشهای ورود روشن کنید.
{`dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://center.abrova.ir/api/packages/abrova/maven") }
}
}`}
سپس وابستگی را در `build.gradle.kts` اپ اضافه کنید:
```kotlin
dependencies {
implementation("ir.abrova:abrova-auth:0.1.1")
}
```
در کنسول ابرووا، **Abrova Auth** را برای پروژه (**تنظیمات › محصولات**) و روشهای ورود دلخواه را (**Abrova Auth › روشهای ورود**) روشن کنید.
AbrovaAuth.shared.configure(context, apiKey) را در Application.onCreate صدا بزنید.
Credential Manager، ID token را برای client ID وبی صادر میکند که بهعنوان serverClientId میدهید. همان client ID وب را در کنسول اضافه کنید و AbrovaNonce.sha256(rawNonce) را بهعنوان nonce بدهید.
blockingTokenProvider thread خودش را block میکند. آن را فقط به SDKهایی بدهید که بیرون از main thread صدایش میزنند، و هرگز خودتان آن را روی main thread صدا نزنید.
{`@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/`}
سپس نصب کنید:
```bash
npm install @abrova/auth-web
```
یا در `package.json`:
```json
{
"dependencies": {
"@abrova/auth-web": "^0.1.1"
}
}
```
در کنسول ابرووا، **Abrova Auth** را برای پروژه (**تنظیمات › محصولات**) و روشهای ورود دلخواه را (**Abrova Auth › روشهای ورود**) روشن کنید.
این رفتار پیشفرض است: نشست در حافظه نگه داشته میشود. اگر صفحهتان Content Security Policy سختگیرانه دارد، storage: localStorageStorage() را بدهید، یا دوباره بهصورت ناشناس وارد شوید.
نشست ذخیرهشده بهصورت async بارگذاری میشود. منتظر auth.ready() یا اولین فراخوانی onAuthStateChanged بمانید.
redirectUrl باید با یکی از آدرسهای لینک در Abrova Auth › تنظیمات شروع شود.
{`@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/`}
سپس آن را همراه یکی از کتابخانههای حافظهی امن نصب کنید:
```bash
npm install @abrova/auth-react-native react-native-keychain
# or, with Expo
npx expo install @abrova/auth-react-native expo-secure-store
```
یا در `package.json`:
```json
{
"dependencies": {
"@abrova/auth-react-native": "^0.1.1"
}
}
```
برای nonce ورود با Apple و Google، `react-native-get-random-values` را هم نصب کنید و آن را در ابتدای `index.js` import کنید:
```bash
npm install react-native-get-random-values
```
```typescript
// index.js, before anything else
import 'react-native-get-random-values';
```
در کنسول ابرووا، **Abrova Auth** را برای پروژه (**تنظیمات › محصولات**) و روشهای ورود دلخواه را (**Abrova Auth › روشهای ورود**) روشن کنید.
storage: keychainStorage(Keychain) یا secureStoreStorage(SecureStore) را بدهید. بدون آن، نشست فقط در حافظه نگه داشته میشود.
crypto.getRandomValues وجود ندارد. react-native-get-random-values را نصب کنید و آن را در ابتدای index.js import کنید.
redirectUrl باید با یکی از آدرسهای لینک در Abrova Auth › تنظیمات شروع شود، مثلاً myapp://auth.
| Level | Description |
|---|---|
| none | No logging output |
| error | Only error messages |
| warning | Errors and warnings |
| info | Errors, warnings, and info messages |
| debug | All messages (recommended for development) |
| verbose | Everything including stack traces |
| Event | Description | Properties |
|---|---|---|
| SDK_INITIALIZED | SDK was initialized | - |
| DEVICE_REGISTERED | Device registered successfully | deviceId, userId |
| DEVICE_UNREGISTERED | Device was unregistered | deviceId |
| MESSAGE_RECEIVED | Push message received | messageId, title, silent, hasImage |
| MESSAGE_DISPLAYED | Notification was shown to user | messageId, title |
| NOTIFICATION_CLICKED | User tapped notification | messageId, title, action |
| ACTION_CLICKED | User tapped action button | actionId, messageId |
| CONNECTED | Connected successfully | - |
| DISCONNECTED | Disconnected | - |
| CONNECTION_ERROR | Connection error occurred | errorCode, errorMessage, details |
| RETRY_STARTED | Reconnection attempt started | retryAttempt, nextRetryMs |
| TOPIC_SUBSCRIBED | Subscribed to topic | topic |
| TOPIC_UNSUBSCRIBED | Unsubscribed from topic | topic |
| NETWORK_STATE_CHANGED | Network state changed | isAvailable, networkType |
| APP_STATE_CHANGED | App visibility changed | isVisible, visibilityState |
| PERMISSION_REQUESTED | Permission prompt shown | - |
| PERMISSION_GRANTED | User granted permission | - |
| PERMISSION_DENIED | User denied permission | - |
Total
Messages sent
Delivered
Successfully
Failed
Delivery failures
Rate
Success %
Use {`{{variable}}`} placeholders that get replaced at send time with real data
Define per-locale title and body; choose the locale per send
One template, many sends. Update the template and all future sends use the new content
Order #ORD-12345 Shipped!
Hi John, your order is on the way to 123 Main St.
Title: Welcome, {`{{name}}`}!
Body: Thanks for joining. Start exploring at {`{{appUrl}}`}
Variables: name, appUrl
Title: Order #{`{{orderId}}`} shipped!
Body: Hi {`{{username}}`}, your package is on the way to {`{{address}}`}.
Variables: orderId, username, address
Title: Payment of {`{{amount}}`} received
Body: Your payment for invoice #{`{{invoiceId}}`} was successful.
Variables: amount, invoiceId
Title: Reminder: {`{{eventName}}`} tomorrow
Body: Hi {`{{userName}}`}, your meeting with {`{{hostName}}`} is at {`{{time}}`}.
Variables: eventName, userName, hostName, time
Title: New assignment in {`{{courseName}}`}
Body: {`{{teacherName}}`} posted "{`{{assignmentTitle}}`}". Due: {`{{dueDate}}`}
Variables: courseName, teacherName, assignmentTitle, dueDate
Title: Your order from {`{{restaurantName}}`}
Body: {`{{driverName}}`} is on the way! Estimated arrival: {`{{eta}}`}
Variables: restaurantName, driverName, eta
platform
android, ios, web
userId
User identifier
appVersion
App version string
country
Country code
language
Device language
metadata.*
Custom fields
message.sent
A send request was accepted for delivery (fired once per message, with success/failed counts)
message.delivered
A device confirmed receipt (fired once per device)
message.failed
The send failed (fired once per message)
message.no_recipients
No device matched the target — message not attempted
device.registered
New device registered
device.updated
Existing device re-registered or re-activated
device.unregistered
Device unregistered or deactivated
Multiple Message Types
Modal, Banner, Fullscreen, and Card formats
Smart Triggers
On app open, custom events, or session start, within an optional start/end date window
User Targeting
Target by segments, user IDs, or properties
Built-in Analytics
Track impressions, clicks, and conversions
Centered popup dialog that requires user action. Best for important announcements or confirmations.
Top or bottom bar that doesn't interrupt the user flow. Good for tips and non-urgent messages.
Full screen takeover for maximum impact. Ideal for onboarding or major announcements.
Floating card notification that appears in corner. Perfect for contextual tips.
Welcome new users and guide them through your app.
Introduce new features to existing users.
Show tips when users visit specific screens.
Run limited-time offers with scheduling.
Cross-sell after a successful purchase.
Target specific high-value users.
Persistent Storage
Messages are stored and accessible anytime, unlike ephemeral push notifications
Read Tracking
Know when users read your messages with built-in analytics
Category Filtering
Organize messages by category (promotions, updates, alerts)
User Control
Users can archive, delete, or mark messages as read
Product updates, new features, company news that users should be able to reference later.
Special offers, discounts, and deals with expiration dates that users can redeem at their convenience.
Order updates, account alerts, and important notifications that need to be preserved.
Send a message to a specific user by their ID
Send a message to a specific device by its device ID
Send to multiple users by providing a comma-separated list of user IDs
Send to all users of your app at once
abrova_sync ^0.3.0
@abrova/sync-react-native
@abrova/sync-web
abrova-sync-android:0.3.0
AbrovaSync 0.3.0 (SPM)
@abrova/sync-node
No install required
API Key
From Console → Your Org → Project. Safe to embed in mobile / web apps.
Server Secret
Only for server-side (Node.js / REST). Never embed in a client.
Top-level container. Created in the Abrova Console — you reference it from code by name (unique within a project).
A named group of documents. Created on first write — no migrations.
A JSON object with a unique ID. Add nested objects, arrays, numbers, anything JSON-serializable.
Filter rows by field. Chain multiple where() calls to AND them.
Sort by one field, ascending or descending. A second orderBy replaces the first.
Page through large result sets without loading everything.
Fires when any document in the collection is added, updated, or removed. Best for short feeds and small lists.
Fires when one specific document changes or is deleted. Best for detail screens.
Same callback shape, but scoped to a .where(...).orderBy(...) chain.
A listener you never cancel keeps an open subscription on the server and continues firing your callback. On screens with their own lifecycle, attach the cancel to dispose() / onDestroy() / deinit.
Update records, counters, and status flags as one atomic transaction so they never drift apart.
Debit one account and credit another together — never one without the other.
Update many related documents together so your data is never in a half-applied state.
Safely move data between collections with all-or-nothing guarantees.
offlineEnabled: true in your config
offlineCacheSizeMb is reserved; not used by the SDKs
serverWins, clientWins, merge, or manual
onSyncState for idle/syncing/offline/error
Optimistic Updates
While offline, writes return immediately with local data
Auto Sync
Pending changes sync automatically when online
Conflict Resolution
Choose strategy: Server wins, Client wins, Merge, or Manual (Android / iOS)
Local Cache
Documents you read and write are kept on the device
serverWins
Server version always wins. Best for shared data where server is source of truth.
clientWins
Client version always wins. Best for personal data or when offline edits should persist.
merge
Automatically merge non-conflicting fields. Best for documents with independent fields.
manual
Your app decides via callback (a conflictResolver on Android and iOS only). Best when you need custom merge logic.
Users can add items to cart while offline. Use merge strategy to combine items added on multiple devices.
Display order status even without network. Use serverWins since order status is controlled by backend.
For inventory-sensitive operations, use serverWins and validate stock levels on sync.
User sees instant feedback (step 3) while sync happens in background (step 4)
Server version always wins. Best for shared data where the server is the source of truth.
Client version always wins. Best for personal data or when offline edits should persist.
Automatically merge non-conflicting fields. Best for documents with independent fields.
Your app decides via a conflictResolver callback (Android and iOS only). Best when you need custom merge logic.
identify(userId, attributes) after sign-in. Server SDKs: pass userId and attributes with each call. Signed-out users are covered by an anonymous id the client SDK keeps for you.>,
},
{
title: 'Read the flag with a default',
body: <>Use isEnabled or a typed getter. The default is returned before the first values arrive, when the flag does not exist, or when it has another type.>,
},
{
title: 'Change it from the dashboard',
body: <>Client SDKs pick up changes on start, when the user changes, and when the app comes back to the foreground. Server SDKs refresh every 30 seconds.>,
},
]} />
### Common Use Cases
DISABLED>,
},
{
title: 'Check flag dependencies',
body: <>Each flag this one depends on is evaluated first, in the same environment and for the same user. If one of them does not give the required result → off, reason PREREQUISITE_FAILED>,
},
{
title: 'Evaluate targeting rules',
body: <>If the flag has targeting rules and they do not match the user context → off, reason NOT_TARGETED>,
},
{
title: 'Rollout bucketing',
body: <>At 100% everyone passes. At 0% nobody does. In between, the user is placed in a bucket by user id (or anonymous id). Outside the percentage → off, reason OUTSIDE_ROLLOUT. No id at all → off, reason NO_BUCKETING_ID>,
},
{
title: 'Variant selection',
body: <>If the flag has variants, one is picked by weight, again from the user id (or anonymous id) → on, reason VARIANT, the value is the variant value>,
},
{
title: 'Return result',
body: <>A flag without variants → on, value true, reason ON>,
},
]} />
### Flow Diagram
```text
┌────────────────────────┐
│ Is flag enabled? │
└────────┬───────────────┘
│ No → off, DISABLED
│ Yes ↓
┌────────────────────────┐
│ Check dependencies │
└────────┬───────────────┘
│ Fail → off, PREREQUISITE_FAILED
│ Pass ↓
┌────────────────────────┐
│ Evaluate targeting │
│ rules │
└────────┬───────────────┘
│ No match → off, NOT_TARGETED
│ Match ↓
┌────────────────────────┐
│ Rollout bucketing │
│ bucket of the user id │
│ inside the rollout %? │
└────────┬───────────────┘
│ Outside → off, OUTSIDE_ROLLOUT
│ No id → off, NO_BUCKETING_ID
│ Inside ↓
┌────────────────────────┐
│ Has variants? │
└────────┬───────────────┘
│ Yes → on, VARIANT (value of the variant)
│ No → on, ON (value true)
```
### Reasons
Every result carries a reason. Read it with the detail getters of the SDKs (`getDetail`, `getStringDetail` and so on).