آخرین بهروزرسانی: ۲۴ ژوئن ۲۰۲۶
برای جایگزینی Visual Studio App Center در یک اپلیکیشن .NET MAUI، پکیج Sentry.Maui را از NuGet نصب کنید، در MauiProgram.cs با builder.UseSentry(...) آن را راهاندازی کنید، فایلهای نمادگذاری (dSYM برای iOS و mapping برای اندروید) را در پایپلاین CI به Sentry آپلود کنید و یک کرش تستی برای تایید جریان داده اجرا کنید. App Center از ۳۱ مارس ۲۰۲۵ بازنشسته شده و سرویس Analytics/Diagnostics آن تا ۳۱ مارس ۲۰۲۷ فعال است؛ یعنی پنجره مهاجرت در حال بسته شدن است و تیمهایی که هنوز پکیج Microsoft.AppCenter.Crashes در کدشان است، در عمل دیتای کرش معتبر دریافت نمیکنند.
چرا App Center بازنشسته شد و چه چیزی جایگزین آن میشود؟
طبق اعلامیه رسمی مایکروسافت در Microsoft Learn، Visual Studio App Center در تاریخ ۳۱ مارس ۲۰۲۵ بازنشسته شد. سرویسهای Build، Test، Distribute و CodePush از آن تاریخ به طور کامل خاموش شدند. تنها استثنا، Analytics و Diagnostics است که تا ۳۱ مارس ۲۰۲۷ بهعنوان دوره گذار فعال نگه داشته شده و مسیر مهاجرت رسمی Microsoft، Azure Monitor با راهحل پیشنمایش موبایل است.
در عمل، تجربه من با تیمهای Xamarin قدیمی که به .NET MAUI مهاجرت کردهاند نشان میدهد که اکثر آنها هنوز پکیج Microsoft.AppCenter.Crashes را در MauiProgram.cs دارند، در حالی که داشبورد App Center دادههای ناقص نشان میدهد. این یک ریسک واقعی است: نسخههایی که در پروداکشن هستند، کرشهایشان عملا گم میشود.
سه گزینه جدی برای جایگزینی Crashes وجود دارد: Sentry با پکیج Sentry.Maui، Firebase Crashlytics با bindingهای جامعه، و BugSnag. در ادامه با معیارهای فنی (نه بازاریابی) مقایسه میکنیم و سپس میرویم سراغ پیادهسازی واقعی. بسیاری از تیمها این مرحله را رد میکنند و فقط میگویند «بعدا یکیشو میگذاریم»؛ نتیجهاش این میشود که اولین کرش بزرگ پروداکشن را از طریق ریویوهای ۱ ستاره استور میفهمید.
مقایسه Sentry، Firebase Crashlytics و BugSnag در .NET MAUI
قبل از انتخاب ابزار، باید بدانید چه چیزی برای یک اپلیکیشن .NET MAUI واقعا مهم است: گرفتن managed exceptionهای .NET با استکتریس کامل، گرفتن کرشهای Native (NSException روی iOS و SIGSEGV روی اندروید)، پشتیبانی از Mac Catalyst و Windows، و یکپارچگی با pipelineهای CI/CD برای آپلود نمادها. جدول زیر دیمنشنهایی را که عملا روی تصمیم اثر میگذارد جمع کرده است.
| ویژگی | Sentry (Sentry.Maui) | Firebase Crashlytics | BugSnag |
|---|---|---|---|
| SDK رسمی برای .NET MAUI | بله (Sentry.Maui) | خیر، فقط binding جامعه | بله |
| پشتیبانی از managed .NET exception | کامل با استکتریس C# | محدود؛ فقط Native trace | کامل |
| پلتفرمهای پشتیبانیشده | iOS، Android، Windows، Mac Catalyst، Tizen | iOS و Android | iOS، Android، Windows |
| Breadcrumbهای خودکار MAUI | بله (lifecycle و UI events) | محدود | بله |
| Performance / Tracing | بله، با TracesSampleRate | Firebase Performance جدا | بله |
| قیمتگذاری | Free tier + پلن تجاری | کاملا رایگان | فقط پولی |
| پیچیدگی setup | کم؛ یک پکیج NuGet | زیاد؛ Plist، JSON و bindingها | متوسط |
اگر اپلیکیشن شما عمدتا کد C# است (که در .NET MAUI طبیعی است)، Sentry انتخاب پیشفرض است چون فقط او میتواند managed exceptionها را با شماره خط واقعی به شما نشان دهد. Firebase Crashlytics زمانی منطق دارد که از قبل Firebase Auth، Remote Config یا Analytics را در پروژه دارید و نمیخواهید سرویس دیگری اضافه کنید؛ با این تفاوت که باید بپذیرید استکتریسهای Native را خواهید دید نه managed.
راهاندازی Sentry.Maui قدمبهقدم
روش کاری من برای راهاندازی Sentry در یک پروژه .NET MAUI همیشه یک چکلیست ثابت است؛ این چکلیست تجربه شکستهای قبلی است.
- پکیج
Sentry.Mauiنسخه پایدار (در زمان نگارش، ۶.۵.۰ و بالاتر) را به پروژه اضافه کنید. - در پنل Sentry، پروژه را با پلتفرم .NET MAUI بسازید و DSN را کپی کنید.
- DSN را در
appsettings.jsonیا User Secrets قرار دهید، هرگز در سورسکنترل commit نکنید. - در
MauiProgram.csباUseSentryآن را راهاندازی کنید وBeforeSendرا برای فیلتر PII تنظیم کنید. - یک هندلر سراسری برای
AppDomain.CurrentDomain.UnhandledExceptionوTaskScheduler.UnobservedTaskExceptionاضافه کنید. - یک Crash Test Button بسازید تا قبل از رفتن به پروداکشن، جریان داده را تایید کنید.
کد نمونه MauiProgram.cs
using Microsoft.Extensions.Logging;
using Sentry;
using Sentry.Maui;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseSentry(options =>
{
options.Dsn = "https://<public-key>@o123.ingest.sentry.io/456";
// Release ı build pipeline e e set kanid:
options.Release = $"com.mycompany.app@{AppInfo.Current.VersionString}+{AppInfo.Current.BuildString}";
options.Environment = DeviceInfo.Current.Platform == DevicePlatform.iOS ? "ios" : "android";
// 0.0 - 1.0 ; production 0.1 - 0.2 monaseb ast
options.TracesSampleRate = 0.2;
options.ProfilesSampleRate = 0.1;
// PII send nashavad:
options.SendDefaultPii = false;
options.BeforeSend = (sentryEvent, hint) =>
{
// Tokenʹha va headerʹhaye Authorization az breadcrumbʹha pak konid
sentryEvent.Request.Headers.Remove("Authorization");
return sentryEvent;
};
// Lifecycle breadcrumbs pishʹfarz roshan hastand
options.IncludeTextInBreadcrumbs = false; // Privacy
});
// Catch-all baraye managed exceptionʹhaye background:
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
SentrySdk.CaptureException((Exception)e.ExceptionObject);
TaskScheduler.UnobservedTaskException += (s, e) =>
{
SentrySdk.CaptureException(e.Exception);
e.SetObserved();
};
return builder.Build();
}
}
گرفتن مدل MVVM exceptionها
در پروژههایی که با CommunityToolkit.Mvvm کار میکنند، خطاهای داخل RelayCommand اگر await بشوند اما لاگ نشوند، در crash dashboard ظاهر نمیشوند. الگوی من یک پایه ViewModel است که خطاها را به Sentry تحویل میدهد:
public abstract partial class BaseViewModel : ObservableObject
{
protected async Task ExecuteSafelyAsync(Func<Task> action, string operation)
{
var transaction = SentrySdk.StartTransaction(operation, "viewmodel");
try
{
await action();
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex, scope =>
{
scope.SetTag("vm", GetType().Name);
scope.SetTag("operation", operation);
});
throw;
}
finally
{
transaction.Finish();
}
}
}
نمادگذاری dSYM و mapping برای استکتریس قابل خواندن
اینجا جایی است که اکثر تیمها رد میکنند و بعد میگویند «گزارشهای Sentry بیفایدهاند». وقتی dotnet publish -c Release روی iOS اجرا میکنید، کامپایلر AOT متدها را به آدرسهای حافظه تبدیل میکند. بدون فایلهای dSYM آپلود شده در Sentry، استکتریس شما چیزی شبیه 0x10484c2a4 خواهد بود. روی اندروید، اگر R8 یا ProGuard فعال باشد، نام کلاسها به a.b.c تبدیل میشود و بدون فایل mapping.txt قابل بازسازی نیست.
نصب Sentry CLI
# macOS / Linux
curl -sL https://sentry.io/get-cli/ | sh
# Windows (PowerShell)
iwr https://sentry.io/get-cli/ -OutFile install.ps1
.\install.ps1
# verify
sentry-cli --version
تنظیم متغیرهای محیطی
export SENTRY_AUTH_TOKEN="sntrys_xxxxxxxxxxxxx"
export SENTRY_ORG="my-org"
export SENTRY_PROJECT="maui-app"
آپلود dSYM و فایلهای PDB
# iOS: faylʹhaye dSYM bad az build dar bin/Release/net8.0-ios/ios-arm64/MyApp.app.dSYM hastand
sentry-cli debug-files upload \
--include-sources \
bin/Release/net8.0-ios/ios-arm64/MyApp.app.dSYM
# Android: ProGuard mapping
sentry-cli debug-files upload \
--type=proguard \
bin/Release/net8.0-android/mapping.txt
# .NET PDB ha (baraye managed stack trace ba line number)
sentry-cli debug-files upload \
--include-sources \
bin/Release/net8.0-android/**/*.pdb
یکپارچهسازی با CI/CD: آپلود خودکار در GitHub Actions
قاعده ساده DevOps: هر کاری که دستی انجام میدهید، یک روز فراموش میشود. آپلود نمادها باید بخشی از همان pipeline build و انتشار باشد، نه یک مرحله جداگانه که شخصی روی لپتاپ خود اجرا کند. اگر هنوز pipeline جامعی ندارید، ابتدا راهنمای CI/CD برای .NET MAUI با GitHub Actions را مرور کنید و سپس مراحل زیر را به آن اضافه کنید.
name: build-and-release
on:
push:
tags: [ 'v*' ]
jobs:
ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Install MAUI workloads
run: dotnet workload install maui
- name: Build iOS Release
run: dotnet publish src/MyApp/MyApp.csproj -c Release -f net9.0-ios /p:ArchiveOnBuild=true
- name: Install Sentry CLI
run: curl -sL https://sentry.io/get-cli/ | sh
- name: Upload dSYM to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: |
sentry-cli debug-files upload \
--include-sources \
src/MyApp/bin/Release/net9.0-ios/ios-arm64/
- name: Create Sentry Release
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
VERSION="com.mycompany.app@${{ github.ref_name }}"
sentry-cli releases new "$VERSION"
sentry-cli releases set-commits "$VERSION" --auto
sentry-cli releases finalize "$VERSION"
سه نکتهای که در پروژههای واقعی همیشه فراموش میشود: (۱) Release name باید دقیقا با options.Release در کد همخوان باشد وگرنه نمادگذاری match نمیشود؛ (۲) set-commits --auto به Sentry اجازه میدهد کرشها را به commitها لینک کند تا متهم اصلی را پیدا کنید؛ (۳) برای انتشار به TestFlight، آپلود dSYM را قبل از فرستادن به App Store Connect انجام دهید، نه بعد، چون Apple فایلهای dSYM را بازنویسی میکند.
پیادهسازی Firebase Crashlytics در .NET MAUI
اگر اپلیکیشن شما به دلایل سازمانی باید با Firebase ادغام شود (مثلا Auth یا Remote Config)، Crashlytics هنوز یک گزینه است، اما با محدودیتهای جدی. SDK رسمی برای .NET MAUI وجود ندارد؛ در عمل از Plugin.Firebase.Crashlytics یا bindingهای جامعه استفاده میکنید.
تنظیم فایلهای Native
GoogleService-Info.plistبرای iOS وgoogle-services.jsonبرای اندروید را به ریشه پروژه اضافه کنید.- روی
GoogleService-Info.plistراستکلیک کنید و Build Action را BundleResource بگذارید. - برای
google-services.json، Build Action را GoogleServicesJson بگذارید. - پکیج
Plugin.Firebase.Crashlyticsرا نصب کنید.
builder
.UseMauiApp<App>()
.RegisterFirebaseServices();
CrossFirebaseCrashlytics.Current.SetCrashlyticsCollectionEnabled(true);
// Managed exception ra dasti taghdim mikonid:
try
{
DoRiskyWork();
}
catch (Exception ex)
{
CrossFirebaseCrashlytics.Current.RecordException(ex);
throw;
}
Performance Monitoring و Release Health
گزارش کرش بهتنهایی فقط نوک کوه یخ است. سلامت واقعی اپ را با سه متریک باید دنبال کنید: Crash-Free Sessions (درصد سشنهایی که بدون کرش تمام میشوند، هدف معقول ۹۹.۵٪+)، ANR Rate روی اندروید، و نرخ adoption نسخه. Sentry این هر سه را زیر تب Release Health نشان میدهد، اما فقط در صورتی که options.Release در کد و pipeline درست تنظیم شده باشد.
برای رصد عملکرد، میتوانید transactionهای دستی برای جریانهای مهم تعریف کنید، مثلا login، sync اولیه، یا checkout. اگر اپ شما از معماری Offline-First استفاده میکند، transactionها برای صف Outbox و sync بسیار ارزشمندند. میتوانید الگوی کامل را در راهنمای معماری Offline-First در .NET MAUI ببینید و سپس هر مرحله sync را با StartTransaction آغاز کنید.
یک نکته دیگر در مورد startup: اگر روی بهینهسازی زمان راهاندازی کار میکنید، تنظیمات Sentry را تا بعد از فاز Critical Render به تاخیر بیندازید. در راهنمای بهینهسازی عملکرد و NativeAOT توضیح دادهام که چطور باید SDKهای telemetry را با lazy init بار کنید تا cold start زیر یک ثانیه بماند.
گافهای رایج مهاجرت از App Center
پنج اشتباهی که تقریبا در هر پروژه مهاجرتشده دیدهام:
- باقی ماندن پکیج
Microsoft.AppCenter.Crashesدر csproj: این پکیج هنوز در NuGet موجود است اما عملا کار نمیکند. حتما حذفش کنید. در یک پروژه دیدم که دو SDK کرش همزمان نصب بودند و یکی unhandled exception را قبل از دیگری میخورد. - نگه داشتن همان user identifier: اگر در App Center با
SetUserIdکار میکردید، در Sentry باید باSentrySdk.ConfigureScope(scope => scope.User = new User { Id = ... })انجام دهید. این کار را در همان نقطهای انجام دهید که توکن JWT load میشود، نه در App.xaml.cs، در غیر این صورت کرشهای اولین سشن بدون شناسه کاربر میآیند. - فراموش کردن آپلود نماد در PRهای hotfix: هر بیلدی که به استور میرود باید نمادهایش هم آپلود شده باشد، حتی hotfixهای کوچک. اگر این کار را به دست انسان بسپارید، یک روز کسی فراموش میکند و آن دقیقا روزی است که کرش بزرگ رخ میدهد.
- تنظیم
SendDefaultPii = trueبدون بررسی GDPR: این flag IP و username را میفرستد. در اپلیکیشنهایی که در اروپا کار میکنند، این میتواند نقض GDPR باشد. بهجایشSendDefaultPii = falseبگذارید و فقط شناسههای pseudo-anonymous را manual ست کنید. - تست نکردن جریان قبل از انتشار: یک Debug-only button بسازید که
throw new InvalidOperationException("sentry-test")اجرا کند و قبل از هر ریلیز ماژور، در داشبورد چک کنید که event با release صحیح و symbolicated رسیده.
پرسشهای متداول
App Center دقیقا چه زمانی بازنشسته شد و چه چیزی هنوز کار میکند؟
سرویسهای Build، Test، Distribute و CodePush در ۳۱ مارس ۲۰۲۵ خاموش شدند. Analytics و Diagnostics بهعنوان دوره گذار تا ۳۱ مارس ۲۰۲۷ فعال نگه داشته شده، اما مایکروسافت توصیه میکند مهاجرت به Azure Monitor یا ابزار شخص ثالث مثل Sentry را همین حالا انجام دهید.
آیا Firebase Crashlytics میتواند managed exceptionهای .NET MAUI را بگیرد؟
به طور محدود. SDK Crashlytics پایهای native است و .NET stack trace را به طور خودکار parse نمیکند. شما باید با RecordException دستی exception را تحویل دهید، اما حتی در این حالت، استکتریس عمدتا فریمهای runtime را نشان میدهد نه نام متدهای C# شما.
چرا کرشهای iOS من در Sentry به صورت آدرس حافظه نمایش داده میشوند؟
فایلهای dSYM آپلود نشدهاند یا Release name در کد با Release name در آپلود یکی نیست. با sentry-cli debug-files upload فایلهای dSYM را آپلود کنید و مطمئن شوید مقدار options.Release دقیقا با نامی که در pipeline میسازید match میکند.
چگونه مهاجرت از Microsoft.AppCenter.Crashes به Sentry.Maui را انجام دهیم؟
پکیج App Center را از csproj حذف کنید، تمام فراخوانیهای Crashes.TrackError را به SentrySdk.CaptureException و Analytics.TrackEvent را به SentrySdk.CaptureMessage تبدیل کنید. سپس AppCenter.Start(...) در MauiProgram.cs را با UseSentry(...) جایگزین کنید و pipeline آپلود dSYM را اضافه کنید.
آیا Sentry در حالت پروداکشن گران میشود؟
هزینه Sentry بر اساس تعداد eventها است. با TracesSampleRate بین ۰.۱ تا ۰.۲ و Dynamic Sampling فعال، اپلیکیشنهای متوسط معمولا در tier پایه میمانند. کرشها همیشه نمونهگیری میشوند (بدون sample rate)، فقط transactionهای تجمعی هستند که هزینه میسازند.


