apptracer_flutter 0.2.1
apptracer_flutter: ^0.2.1 copied to clipboard
Unofficial Flutter integration with Tracer (apptracer.ru): reports Dart errors, Flutter framework errors and breadcrumbs that the native Tracer SDKs cannot see on their own.
apptracer_flutter #
Неофициальная интеграция Flutter с Tracer — сервисом мониторинга ошибок от OK.TECH / VK.
Это не официальный SDK. Пакет не связан с VK и OK.TECH, не одобрен и не поддерживается ими. Это независимая обёртка над публичными SDK вендора; сами SDK пакет не распространяет. С проблемами обращайтесь сюда, а не в поддержку Tracer.
English version: README.en.md.
Быстрый старт #
Пять минут до первого события в консоли Tracer. Развёрнутые версии всех шагов — ниже по документу.
1. Заведите проект в консоли Tracer. Отдельный на
каждую платформу. Каждый проект выдаёт свою пару — appToken и
pluginToken: приложение на Android, iOS и web означает три проекта и три
пары. Оба значения лежат в разделе Настройки → Проект → API.
Нужны они в разное время. appToken — приложению, чтобы отправлять события;
без него ничего не работает. pluginToken — сборке, чтобы залить символы, и до
первого релиза он не нужен вовсе.
2. Подключите SDK Tracer к сборке. Пакет — обёртка: сами SDK вендора он не распространяет и за собой не тянет, их добавляет приложение.
Android #
В android/settings.gradle.kts сохраните репозиторий вендора для SDK 1.5.2
и Gradle-плагина вместе с Maven Central:
pluginManagement {
repositories {
google()
mavenCentral()
maven("https://nexus-external.vkteam.ru/repository/maven/") {
content { includeGroup("ru.ok.tracer") }
}
gradlePluginPortal()
}
}
plugins {
id("ru.ok.tracer") version "1.5.2" apply false
}
Добавьте тот же репозиторий для зависимостей (allprojects.repositories
в android/build.gradle.kts либо dependencyResolutionManagement.repositories,
если репозитории управляются через settings):
maven("https://nexus-external.vkteam.ru/repository/maven/") {
content { includeGroup("ru.ok.tracer") }
}
В android/app/build.gradle.kts:
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
id("ru.ok.tracer")
}
android {
// SDK читает appToken из ресурса, который генерируется при сборке.
// С AGP 9 фича выключена по умолчанию, и без неё SDK падает в рантайме.
buildFeatures {
resValues = true
}
}
tracer {
create("defaultConfig") {
appToken = "ANDROID_APP_TOKEN"
pluginToken = providers.gradleProperty("androidPluginToken").orNull
uploadMapping = true
uploadNativeSymbols = true
}
}
dependencies {
implementation(platform("ru.ok.tracer:tracer-platform:1.5.2"))
implementation("ru.ok.tracer:tracer-crash-report")
// По желанию, для нативных крашей:
implementation("ru.ok.tracer:tracer-crash-report-native")
}
Токен на Android приходит только отсюда: альтернативы в рантайме нет, поэтому Gradle-плагин обязателен.
Два ключа подставляются по-разному не для красоты. appToken плагин всё равно
вшивает в APK, прятать нечего — пусть лежит строкой. pluginToken подписывает
загрузку маппингов и символов, в приложение не попадает и в репозитории ему не
место.
Положите его в ~/.gradle/gradle.properties:
androidPluginToken=...
Этот файл лежит вне репозитория, и Gradle читает его сам — способ запуска не
меняется ничем: и flutter build, и кнопка Run в IDE увидят значение.
В CI файла нет, и заводить его не надо: то же свойство приходит переменной
ORG_GRADLE_PROJECT_androidPluginToken — Gradle подставляет такие переменные в
свойства проекта, — или флагом -PandroidPluginToken=…. В GitHub Actions:
- run: flutter build apk --release
env:
ORG_GRADLE_PROJECT_androidPluginToken: ${{ secrets.ANDROID_PLUGIN_TOKEN }}
Ещё четыре момента, о которые легко споткнуться:
TracerOptions.appTokenна Android игнорируется. Токен приходит из Gradle-плагина. Если передать его всё равно, плагин напишет предупреждение, а не сделает вид, что значение применилось.- По умолчанию SDK не отправляет данные из debug-сборок. Включить это можно
только из своего
Application, реализующегоHasTracerConfiguration(setDebugUpload), — и такойApplicationзаменяет настройки пакета целиком. Чтобы не потерять рейт-лимит, наследуйте его отru.apptracer.flutter.TracerApplicationи добавляйте своё кsuper.tracerConfiguration. Tracer.stopCollection()вызываетTracer.disable()нативного SDK, а его нельзя отменить до перезапуска процесса. Это сделано намеренно, см. privacy.md.TracerOptions.environmentна Android тоже игнорируется — SDK берёт его из Gradle-плагина, по умолчанию это имя build variant. Задавайте в блокеtracer { }.
Android tombstones (SDK 1.5.2)
Экспериментальные системные отчёты о нативных крэшах выключены по умолчанию.
Чтобы включить их без собственного Application, добавьте внутрь <application>
в android/app/src/main/AndroidManifest.xml:
<meta-data
android:name="ru.apptracer.flutter.experimentalTombstoneEnabled"
android:value="true" />
Нужны Tracer Android 1.5.2 и Android 12 / API 31+. Функция входит
в tracer-crash-report; модуль tracer-crash-report-native необязателен
и остаётся полезным для нативных крэшей на старых версиях Android. Tombstones
позволяют символизировать системные библиотеки, но не гарантируют символизацию
Dart AOT. Отчёты восстанавливаются при последующем запуске SDK.
Собственный Application, реализующий HasTracerConfiguration, заменяет
настройки пакета. В его существующем CrashReportConfiguration.Builder вызовите
setExperimentalTombstoneEnabled(true) и при необходимости сохраните лимит
нефатальных событий. Наследник TracerApplication учитывает флаг манифеста.
Флаг работает при автоматическом и отложенном запуске. Он задаётся на Android
до инициализации SDK, а не через Dart TracerOptions. Для отложенного сбора
по-прежнему нужны удаление провайдеров из манифеста и явный startCollection;
сам флаг сбор не запускает. Сохраните nexus-external.vkteam.ru/repository/maven/
в репозиториях плагинов и зависимостей и выполните чистую сборку после обновления.
iOS #
Работают оба менеджера зависимостей. На Swift Package Manager настраивать
нечего: Package.swift пакета сам объявляет OKTracer зависимостью от
репозитория вендора. Разница одна — фазу выгрузки dSYM там некому добавить
при pod install, поэтому её ставят один раз командой:
dart run apptracer_flutter:install_ios_dsym_phase
Дальше про токен читайте так же, как ниже; всё остальное в этом разделе — про CocoaPods.
В ios/Podfile — OKTracer лежит в spec-репозитории вендора, а поставляется
статическим xcframework, поэтому нужны и свой источник, и смена типа
линковки:
source 'https://github.com/odnoklassniki/tracer-ios.git'
source 'https://cdn.cocoapods.org/'
platform :ios, '13.0'
target 'Runner' do
use_frameworks! :linkage => :static # было: use_frameworks!
# ...
end
Затем pod install. appToken передаётся из Dart, шагом ниже.
Для читаемых стектрейсов нативных крашей нужна загрузка dSYM. При
pod install пакет добавляет в Runner.xcodeproj фазу их автоматической
загрузки при каждой release-сборке. Укажите pluginToken iOS-проекта
одним из двух способов:
Первое — файл ios/tracer_plugin_token, рядом с Podfile. Создайте его и
положите внутрь одну строку: pluginToken iOS-проекта из консоли Tracer.
Выглядит файл так:
e4f1b0c2-8a7d-4c19-9f3e-2b6d5a0c7e18
Файл содержит секрет, поэтому добавьте его в .gitignore.
Второе — переменная окружения, и в CI обычно берут её: фаза читает
TRACER_IOS_PLUGIN_TOKEN, а если её нет — TRACER_PLUGIN_TOKEN. Тогда файл не
нужен.
Без токена фаза пишет предупреждение и пропускает загрузку, а при неудачной отправке — предупреждение и продолжает: ронять архив из-за сетевой ошибки хуже, чем собрать его без символов.
Отключается двумя способами: удалить фазу в Xcode (она подписана
[apptracer_flutter]) или выставить TRACER_SKIP_IOS_PHASE=1 — тогда
pod install не тронет файл проекта.
Если релиз собирается в CI и хочется, чтобы пайплайн падал, когда символы не уехали, вызовите ту же загрузку явно — эта команда возвращает ненулевой код:
flutter build ipa
export TRACER_PLUGIN_TOKEN=IOS_PLUGIN_TOKEN
dart run apptracer_flutter:upload_symbols ios --app-name=Runner
Xcode 27: проверенная настройка DWARF 4.
Наконец, тот же запрос вручную — если ничего ставить не хочется:
archive="$(pwd)/build/ios/archive/Runner.xcarchive"
plist="$archive/Products/Applications/Runner.app/Info.plist"
(cd "$archive/dSYMs" && zip -qry /tmp/dsym.zip ./*.dSYM)
curl --location --http1.1 \
--form versionName="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "$plist")" \
--form versionCode="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist")" \
--form file=@/tmp/dsym.zip \
"https://plugin-api.apptracer.ru/api/symbol/upload?symbolToken=IOS_PLUGIN_TOKEN"
Ответ {"success":true} — принято.
Вручную это делается на каждый релиз и до первых крашей. У каждой сборки
свои dSYM с собственными UUID, поэтому символы прошлой версии новой не
подходят, а версия читается из собранного Info.plist, а не пишется руками:
разойдётся с той, что шлёт приложение, — символы молча лягут к другой версии.
Пересимволизации у Tracer нет: символы применяются только к событиям,
полученным после загрузки.
Web #
Добавлять нечего: реализация на чистом Dart уже внутри пакета. Токен —
appToken JS-проекта, передаётся шагом ниже.
pluginToken JS-проекта, как и на iOS, нужен не для событий, а для загрузки
сорсмап — без них стектрейс из release-сборки остаётся минифицированным. Тут у
вендора инструмента нет, поэтому команда пакета — основной путь:
flutter build web --release --source-maps
dart run apptracer_flutter:upload_symbols web --token=WEB_PLUGIN_TOKEN
Она берёт из build/web только .js и .map, пакует их так, чтобы пути
совпали с путями в кадрах, и подставляет версию из pubspec.yaml — она должна
совпасть с release в TracerOptions.
Тот же запрос вручную:
flutter build web --release --source-maps
cd build/web && zip -qr /tmp/sourcemaps.zip . -i '*.js' '*.map'
curl --location \
-F sourcemapToken=WEB_PLUGIN_TOKEN \
-F versionName=1.0.0 \
-F file=@/tmp/sourcemaps.zip \
https://plugin-api.apptracer.ru/api/sourcemap/upload
Как и на iOS — на каждый релиз и до выкладки: сорсмапы применяются только к тому, что пришло после их загрузки.
3. Оберните запуск приложения.
import 'package:apptracer_flutter/apptracer_flutter.dart';
void main() {
Tracer.initialize(
options: const TracerOptions(
iosAppToken: 'IOS_APP_TOKEN',
webAppToken: 'WEB_APP_TOKEN',
),
appRunner: () => runApp(const MyApp()),
);
}
Поля Android здесь нет: его SDK читает токен из ресурса, который создаёт
Gradle-плагин, и переопределить это из Dart нечем. Если платформа одна, хватит
общего appToken — он используется там, где своего не задано.
appRunner — это запуск вашего приложения, отданный пакету функцией: обычно
() => runApp(const MyApp()). Пакет вызывает её сам, уже внутри охраняемой
зоны. Только так в зону попадают асинхронные ошибки, которых никто не
await-ил, и только так WidgetsFlutterBinding.ensureInitialized() оказывается
в той же зоне, что и runApp, — иначе Flutter пожалуется на несовпадение зон.
Всё, что бросается дальше, — необработанные исключения, ошибки внутри build()
и асинхронные ошибки без await — уходит в Tracer само. Ничего больше вызывать
не нужно.
4. Убедитесь, что связка живая. Повесьте на кнопку строку и нажмите:
onPressed: () => throw StateError('проверка apptracer_flutter'),
В консоли Tracer должно появиться событие. Заголовок у платформ разный, и это
нормально: на Android он читается как
DartError: StateError: проверка apptracer_flutter, на iOS консоль всегда
подставляет в начало свой разбор верхнего нативного кадра, поэтому там будет
+ 0 - StateError: проверка apptracer_flutter. Читаемый стектрейс Dart в обоих
случаях лежит во вкладке «Логи».
Если событий нет #
Почти всегда дело в одном из пяти:
- Платформенная настройка пропущена. При старте пакет печатает строку о
том, что он выключен и почему;
Tracer.isEnabledв этот моментfalse. Загляните в лог первым делом. - Сборка debug. Нативный SDK по умолчанию не отправляет ничего из
debug-сборок — ни с Android, ни с iOS. Проверяйте на release, либо включайте
setDebugUpload(Android, см. выше). - Android: не выставлен
resValues = true. Начиная с AGP 9 фича выключена по умолчанию, а SDK читаетappTokenименно из сгенерированного ресурса и падает в рантайме без него. - Android: токен передан в
TracerOptions. Там он игнорируется — на Android токен берётся только из блокаtracer { }в Gradle. - iOS или web:
appTokenприехал пустым. Чаще всего это выбранный вариант с--dart-define, где флаг забыли при сборке:String.fromEnvironmentбез него возвращает пустую строку. Пакет честно сообщает, чтоappTokenне задан, и остаётся выключенным.
Что дальше #
- Использование — ручная отправка, breadcrumbs, кастомные ключи.
- Согласие пользователя — как не собирать ничего, пока пользователь не разрешил.
- Какие данные уходят — полный список, включая то, что добавляет от себя нативный SDK.
- Release-сборки со
--split-debug-info— что станет со стектрейсом и как его прочитать.
Зачем это нужно #
Нативные SDK Tracer не видят ошибок Dart.
Android SDK ставит Thread.UncaughtExceptionHandler, нативный обработчик
сигналов и ANR-watchdog. Необработанное Dart-исключение не задевает ни один из
них: процесс не падает, и в JVM исключение не попадает. Flutter перехватывает
его внутри Dart — через FlutterError.onError,
PlatformDispatcher.instance.onError или обработчик ошибок guarded zone. На iOS
то же самое.
Для Flutter-приложения это подавляющее большинство ошибок. Подключите Tracer SDK сам по себе — и получите нативные краши и ANR при подозрительно пустом дашборде: исключения, с которыми реально сталкиваются пользователи, туда просто не доходят.
Пакет цепляется к этим трём точкам входа Dart и передаёт найденное в нативный SDK, который продолжает сам заниматься нативными крашами, ANR и метрикой crash-free.
Сравнение с Firebase Crashlytics #
Вопрос законный: Firebase Crashlytics бесплатен, официален и делает то же самое.
| apptracer_flutter | firebase_crashlytics | |
|---|---|---|
| Чьи SDK и куда уходят отчёты | нативные SDK VK / OK.TECH, приём в их инфраструктуре | SDK Google, приём в инфраструктуре Google |
| Платформы | Android, iOS, web | Android, iOS, macOS |
| Перехват ошибок Dart | FlutterError.onError, PlatformDispatcher.onError, guarded zone |
FlutterError.onError, PlatformDispatcher.onError |
| Обфусцированный Dart | вручную: flutter symbolize по сохранённому файлу символов |
Android — firebase crashlytics:symbols:upload; iOS — автоматически |
| Нативные символы каждой сборки | Android — сам Gradle-плагин; iOS — сборочная фаза, которую пакет прописывает сам; web — команда пакета | Android — командой Firebase CLI; iOS — сборочной фазой Xcode |
Про данные. Это и есть основная причина выбирать Tracer: у Firebase Crashlytics SDK и приём принадлежат Google, у Tracer — VK / OK.TECH, с приёмом в российских сетях, так что трансграничной передачи не происходит — но обезличенным крашлог от этого не становится, персональные данные в него кладёте вы, и что именно уходит с устройства, перечислено в privacy.md.
Что вы получите, кроме ошибок Dart #
Ошибки Dart — работа этого пакета, и они одинаково доезжают со всех платформ. Неровно распределено остальное, потому что этим занимается нативный SDK вендора:
- Android — нативные краши и ANR. ANR только с Android 11:
AnrReporterвtracer-crash-report1.4.0 строит отчёт изApplicationExitInfo, а тот появился в API 30, и нижеsetSendAnr(true)не даёт ничего. - iOS — нативные краши и счётчик зависаний.
- Web — только ошибки Dart, нативных крашей там нет по определению.
На платформе без реализации пакет инертен: isEnabled равен false,
печатается одна диагностическая строка, ничего не бросается, приложение
стартует. Подробности — в
platform-matrix.md.
Использование #
appRunner — та самая функция запуска из шага 3 — вызывается ровно один раз
в любом сценарии: при обычном старте, при запрете сбора политикой, при падении
инициализации нативного SDK и на платформе без реализации. Сборщик ошибок,
способный не дать приложению запуститься, хуже, чем отсутствие сборщика ошибок.
Ручная отправка обработанной ошибки:
try {
await repository.load();
} catch (error, stackTrace) {
await Tracer.recordError(
error,
stackTrace,
severity: TracerSeverity.warning,
issueKey: 'ORDERS-LOAD', // переопределяет группировку
customKeys: {'endpoint': '/orders'},
);
}
Логи и ключи:
Tracer.log('пользователь нажал «оформить»', category: 'ui');
await Tracer.setCustomKey(key: 'checkout_step', value: '3');
await Tracer.removeCustomKey('checkout_step');
Это не аналитика: ни то, ни другое само по себе никуда не уходит. Breadcrumbs —
хронология того, что было перед падением, ключи — срез состояния на его момент;
и то и другое едет прицепом к отчёту об ошибке, а без ошибки просто вытесняется
из буфера. Ключ живёт до конца сеанса, поэтому его и снимают: иначе
checkout_step=3 приедет с падения в настройках и собьёт с толку, а лишние
ключи вытеснят нужные — Tracer держит не больше 30.
Breadcrumbs копятся в Dart и сразу дублируются в нативный лог-буфер, поэтому нативный краш — которого Dart-сторона не видит — всё равно приходит с цепочкой событий.
Согласие пользователя #
Для Android сначала удалите оба startup provider из manifest приложения,
сохранив metadata инициализаторов и Gradle-ресурсы. Точная настройка и ограничения
описаны в контракте нативного сбора.
Одного isCollectionEnabled: false недостаточно для предотвращения запуска SDK
до Flutter.
// Bootstrap: приложение запускается, сбор пока выключен.
await Tracer.initialize(
options: const TracerOptions(
nativeInitialization: TracerNativeInitialization.deferred,
),
appRunner: () => runApp(const MyApp()),
);
// После проверки согласия и его применимости к текущему аккаунту:
final started = await Tracer.startCollection();
if (started.state == TracerCollectionState.restartRequired) {
// Для возобновления нужен новый процесс; повторять initialize не следует.
}
// При отзыве, logout или смене пользователя:
final stopped = await Tracer.stopAndClearCollection();
// error/unsupported означает, что необходимая нативная очистка не подтверждена.
startCollection не вызывает appRunner повторно. Остановка сразу запрещает
новые Dart-события, очищает breadcrumbs и custom keys и снимает установленные
обработчики, если они всё ещё принадлежат пакету. Результат нативной операции
приходит асинхронно; уже отправленные серверу данные она не удаляет.
Android-адаптер 1.4.0 прошёл проверки устройства, сети и release/R8 на API 35.
На iOS отложенный режим поддерживается с OKTracer строго 1.5.2: явный старт
после согласия, очистка и блокировка файлов отчётов после отзыва. После активного
сбора остановка возвращает restartRequired; автоматический запуск в новом
процессе не отменяет отзыв. Новое согласие требует явного deferred-старта.
Проверка жизненного цикла завершена для указанных SDK; публикация пакетов и
миграция нативного канала согласия приложения «Сад» остаются отдельными шагами.
Фильтрация данных #
TracerOptions(
beforeSend: (event) => event.message.contains('@')
? event.copyWith(message: '<скрыто>')
: event,
beforeBreadcrumb: (crumb) => crumb.category == 'auth' ? null : crumb,
)
Верните null, чтобы отбросить событие. Если хук бросит исключение, оно будет
залогировано и проигнорировано, а исходное событие всё равно отправится.
Очень большие стектрейсы #
Дословный стектрейс — исходный текст трейса, как его напечатал Dart, тот самый,
который умеет расшифровать flutter symbolize, — пишется в платформенный лог и
по умолчанию ограничен 8 КБ; разобранные кадры — 128 штуками.
Причина в том, что лог-буфер Android — кольцевой, на 64 КБ: всё
записанное вытесняет что-то более старое. Патологический стектрейс
(StackOverflowError, глубокая цепочка async) занимает сотни килобайт и
вымоет из буфера всю цепочку breadcrumbs — событие приедет со стектрейсом и без
контекста.
Обрезается хвост: начало сохраняется, потому что там заголовок build_id и
кадры ближе всего к месту броска. Сколько строк выкинуто — написано прямо в
логе. Поднять или снять лимит:
TracerOptions(
maxRawStackTraceLogBytes: 32768, // 0 — без лимита
maxStackFrames: 256, // 0 — без лимита
)
Уровень автоматически перехваченных ошибок #
Ошибки из FlutterError.onError, PlatformDispatcher.onError и guarded zone
отправляются с уровнем error, а не fatal. Ни одна из них не завершает
процесс — именно поэтому нативные SDK их и не видят, — а fatal-событие влияет на
метрику crash-free на Android и iOS. Помечать их фатальными значило бы
отчитываться о крашах, которых не было.
Если команда осознанно решила иначе:
TracerOptions(reportUnhandledErrorsAsFatal: true)
Какие данные уходят #
На Android и iOS сам пакет не добавляет никаких персональных данных: ни идентификатора установки, ни идентификатора устройства, ни идентификатора пользователя, ни автоматического контекста. Отправляется либо свойство самой ошибки, либо то, что вы передали явно.
На web иначе. Формат приёма скопирован с JS-SDK вендора, а тот всегда шлёт
deviceId, поэтому реализация заводит его сама — UUID в localStorage, один и
тот же до очистки данных сайта, — и вместе с ним шлёт host, размеры экрана,
угол поворота и visibilityState. Отключить это нечем.
Нативные SDK — отдельная история: Android SDK сам собирает модель устройства, производителя, ABI, версию ОС, оператора связи и пакет установщика, независимо от того, стоит ли этот пакет. Полная таблица и способы ограничения — в privacy.md.
Группировка #
Если вызывающий код не задал issueKey, пакет синтезирует его сам — из типа
ошибки и внутреннего именованного кадра, не длиннее 32 символов.
Это не украшение. На Android Tracer группирует по классу и методу верхнего кадра и только по ним.
Ни файл, ни номер строки в ключ не входят: сам Tracer их игнорирует, чтобы
правка кода не разносила одну проблему по нескольким группам, и синтетический
ключ это свойство сохраняет. Свой issueKey, переданный в recordError, всегда
имеет приоритет.
Release-сборки со --split-debug-info #
При сборке со --split-debug-info — с --obfuscate или без него —
Dart-стектрейсы превращаются в адреса:
build_id: 'b71885097a7ebc4d1ab80642f606c4be'
#00 abs 0000007938a1c2f0 virt 00000000002cc2f0
Пакет по умолчанию отправляет дословный стектрейс вместе с заголовком, поэтому он остаётся расшифровываемым.
Текст лежит в логе события, во вкладке с логом, под строкой
--- apptracer_flutter: verbatim Dart stack trace ---. Скопируйте оттуда сам
трейс — от строки с build_id: до последнего кадра #NN abs … — и сохраните
в файл; служебные строки, которые пакет дописал сверху, включать не нужно.
Дальше:
flutter symbolize -d build/symbols/app.android-arm64.symbols -i trace.txt
Файл символов нужен от той же сборки: build_id в трейсе должен совпасть с
app.<платформа>-<архитектура>.symbols, который --split-debug-info положил
рядом с артефактом. Символы соседнего релиза не подойдут, поэтому архивируйте
их на каждую сборку. Если в конце трейса стоит
... [apptracer_flutter] truncated, N more line(s), лог обрезал хвост:
расшифруются только уцелевшие кадры, а поднять предел можно через
TracerOptions.maxRawStackTraceLogBytes.
У Tracer нет канала для загрузки Dart-файлов --split-debug-info — это
подтвердил вендор, — поэтому расшифровка остаётся ручным шагом. Подробности — в
symbolication.md.
Лицензия #
MIT. SDK вендора лицензируются отдельно, см. legal.md.