# Abrova docs (en)
https://abrova.ir — backend services for app developers in Iran.
## Abrova Trace
- Flutter SDK: https://abrova.ir/en/docs/trace/flutter-sdk
enablePerformanceTracking() record the full request URL, including query strings, so avoid putting secrets in URLs or exclude those hosts with setPerformanceExcludedHosts(_:). The separate AbrovaTraceHttpBreadcrumbProtocol.enable() adds HTTP breadcrumbs with sensitive query parameters (token, api_key, apikey, key, secret, password, pwd, auth, authorization, access_token, refresh_token, session) redacted, and reports 5xx responses and failed requests as messages.
AbrovaTrace.shared.close() itself when it receives the app's will-terminate notification; calling it yourself is optional and flushes buffered logs.
| {h} | ))}||
|---|---|---|
| {endpoint} | {method} | {desc} |
Measures loading performance. Marks when the largest content element (image, video, or text block) becomes visible.
Measures overall responsiveness. Observes all interactions and reports the worst latency.
Measures visual stability. Sum of all unexpected layout shifts during the page's lifecycle.
{name}
{desc}
Good: {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) {'←'} where error occurred
{" "}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() or AbrovaTrace.init()) — no extra call is needed. AbrovaTrace.close() only flushes pending data and shuts the SDK down.
dart2js (the compiler that turns Dart into JavaScript) inlines aggressively in --release builds. Some frames will resolve cleanly to your .dart files. Others will resolve to framework internals (js_helper.dart, dart:async) or stay minified (a3v.$0) because the .map simply contains no mapping for those JS bytes.
This is a property of dart2js output — not the resolver. We tell you up front because most teams hit it in production and assume their setup is broken. It isn't.
Where Abrova pulls ahead is context. Enable gesture breadcrumbs (AbrovaTrace.enableGestureBreadcrumbs()) so every issue shows the widget the user tapped right before the error, plus the route, last HTTP call, and any custom breadcrumbs. Combined with the partially-resolved trace, you get a reproducible bug report.
For QA / staging only, you can force less aggressive optimization to get fully resolved traces:
Bundle size grows ~30% — fine for staging, not recommended for production.
Anytime you ship a production build with minification (Next.js, Vite, webpack, Rollup). Without uploaded sourcemaps, every error in the Abrova Trace dashboard shows the minified stack — useless for debugging.
"I uploaded sourcemaps but errors still show minified stacks."
Usually the release string in the upload doesn't match what the SDK is sending. Check the Abrova Trace dashboard's error detail page for the release version, then re-upload with that exact string.
{d}
{item.value}
{item.label}
| {h} | ))}||
|---|---|---|
| {level} | {count} |
Example journey:
| {h} | ))}|
|---|---|
{type} |
{desc} |
{`{"status": "created", "id": 12345}`}
{`{"status": "created", "count": 3, "projectId": "..."}`}
| {h} | ))}||
|---|---|---|
| {type} | {desc} | {ex} |
{s.desc}
| {h} | ))}||
|---|---|---|
| Resolve Single Error | You've fixed one specific issue | Open the issue → "Resolve" (shortcut R) |
| Resolve All | Multiple fixes deployed at once | Issues → ⋯ → "Resolve all open issues…" |
Subject: {'\u{1F4CA}'} Your Weekly Error Report - Dec 15
Week of December 15, 2025
8
New Errors
12
Resolved
5
Open
47
Occurrences
{'↑'} 33% more than last week
Top Errors This Week:
1. TypeError: Cannot read 'map' of undefined — 23 · open
2. NetworkError: Failed to fetch — 15 · resolved
3. ReferenceError: user is not defined — 9 · open
Subject: [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_...) is also accepted while the project setting Server routes require the server secret is off — but it ships inside your apps, so anyone who extracts it could send a fake "ok" check-in. Switch the setting on once your jobs use the server secret. See Server Secret & Server Routes.
| {h} | ))}|||
|---|---|---|---|
{field} |
{type} | {required === 'Yes' ? Yes : No} | {desc} |
{s.desc}
monitor_slug matches exactlyab_srv_ (or ab_live_)server_credential_required, the project setting Server routes require the server secret is on and the job sent the app key — use the server secretX-API-Key header{`{"success": true, "deploy": { ... }}`}
#### Logs sent from your servers
The [Logs REST API](/docs/trace/logs-rest-api) is never refused because of this setting, but a server should still authenticate with the server secret rather than the app key. The Node.js and Laravel SDKs already send the server secret you configure them with.
The job still sends the app key. Replace it with the server secret (ab_srv_...) in the X-API-Key header, or add an x-server-secret header. Until then the monitor shows the job as missed.
The agent still runs with the app key. Log lines keep arriving, but the agent can no longer register containers, report their status or receive commands. Redeploy it with ABROVA_TRACE_API_KEY set to the server secret.
Update it everywhere it is used: the agent, cron jobs, deploy scripts and backend SDK configuration. The old secret stops working when you rotate.
email1@domain.com/revokemyorg, name abrova-tracegithub.com/myorg/abrova-trace✓ Recommended
```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()); }, ); } ```✗ Basic (misses async errors)
```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`}
Then run `flutter pub get` and sign in:
```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_...
```
The user gets a stable id and a user token from the first launch. Later, link a method to the same user — their data stays theirs:
```dart
await auth.linkPassword(email: 'sara@example.com', password: password);
```
The same on the web, after pointing the `@abrova` scope at Abrova Center in `.npmrc`:
{`@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_...
```
Every platform's page has the full setup: [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`}
Then install it:
```bash
flutter pub get
```
In Abrova Console, turn on **Abrova Auth** for your project (**Settings › Products**) and the sign-in methods you want (**Abrova Auth › Sign-in methods**).
Turn on Abrova Auth under Settings › Products and the method under Abrova Auth › Sign-in methods. Use getMethods() to show only the methods that are on.
Call initialize() before reading currentUser, and sign in only when currentUser is null.
Check that the bundle ID, Services ID or client ID is in the Console, and that you gave the provider AbrovaAuthNonce.sha256Hex(raw) and Abrova the raw value.
They cache their token. Call their refresh method (for example AbrovaSync.refreshUserToken()) from your authStateChanges listener.
Call AbrovaAuth.shared.configure(apiKey:) at app start, before any other call.
Add your app's bundle ID under Abrova Auth › Sign-in methods › Apple, and the Sign in with Apple capability to the target.
Turn on Abrova Auth under Settings › Products and the method under Abrova Auth › Sign-in methods.
{`dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://center.abrova.ir/api/packages/abrova/maven") }
}
}`}
Then add the dependency in your app's `build.gradle.kts`:
```kotlin
dependencies {
implementation("ir.abrova:abrova-auth:0.1.1")
}
```
In Abrova Console, turn on **Abrova Auth** for your project (**Settings › Products**) and the sign-in methods you want (**Abrova Auth › Sign-in methods**).
Call AbrovaAuth.shared.configure(context, apiKey) in Application.onCreate.
Credential Manager issues the ID token to the web client ID you pass as serverClientId. Add that web client ID in the Console, and pass AbrovaNonce.sha256(rawNonce) as the nonce.
blockingTokenProvider blocks its thread. Give it only to SDKs that call it off the main thread, and never call it yourself on the main thread.
{`@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/`}
Then install:
```bash
npm install @abrova/auth-web
```
Or in `package.json`:
```json
{
"dependencies": {
"@abrova/auth-web": "^0.1.1"
}
}
```
In Abrova Console, turn on **Abrova Auth** for your project (**Settings › Products**) and the sign-in methods you want (**Abrova Auth › Sign-in methods**).
That is the default: the session is kept in memory. Pass storage: localStorageStorage() if your page has a strict Content Security Policy, or sign in anonymously again.
The stored session loads asynchronously. Wait for auth.ready() or the first onAuthStateChanged call.
The redirectUrl must start with one of the link addresses under Abrova Auth › Settings.
{`@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/`}
Then install it with one of the secure storage libraries:
```bash
npm install @abrova/auth-react-native react-native-keychain
# or, with Expo
npx expo install @abrova/auth-react-native expo-secure-store
```
Or in `package.json`:
```json
{
"dependencies": {
"@abrova/auth-react-native": "^0.1.1"
}
}
```
For Apple and Google sign-in nonces, also install `react-native-get-random-values` and import it first thing in `index.js`:
```bash
npm install react-native-get-random-values
```
```typescript
// index.js, before anything else
import 'react-native-get-random-values';
```
In Abrova Console, turn on **Abrova Auth** for your project (**Settings › Products**) and the sign-in methods you want (**Abrova Auth › Sign-in methods**).
Pass storage: keychainStorage(Keychain) or secureStoreStorage(SecureStore). Without it, the session is kept in memory only.
crypto.getRandomValues is missing. Install react-native-get-random-values and import it first in index.js.
The redirectUrl must start with one of the link addresses under Abrova Auth › Settings, for example 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).