گزارش‌گیری Crash در .NET MAUI پس از بازنشستگی App Center: راهنمای مهاجرت به Sentry در ۲۰۲۶

App Center در مارس ۲۰۲۵ بازنشسته شد. در این راهنما، مهاجرت گزارش‌گیری کرش .NET MAUI به Sentry را با نصب Sentry.Maui، آپلود dSYM، یکپارچه‌سازی GitHub Actions و مقایسه با Firebase Crashlytics قدم‌به‌قدم می‌بینید.

گزارش کرش .NET MAUI با Sentry (۲۰۲۶)

آخرین به‌روزرسانی: ۲۴ ژوئن ۲۰۲۶

برای جایگزینی 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 در ۳۱ مارس ۲۰۲۵ بازنشسته شد و فقط Analytics & Diagnostics تا مارس ۲۰۲۷ پشتیبانی می‌شود؛ مهاجرت دیگر اختیاری نیست.
  • برای .NET MAUI، گزینه پیشنهادی Sentry با پکیج رسمی Sentry.Maui است که هم managed exception و هم کرش‌های Native iOS/Android را پوشش می‌دهد.
  • Firebase Crashlytics در .NET MAUI SDK رسمی ندارد و .NET stack trace را گم می‌کند؛ فقط برای پروژه‌هایی که از قبل با Firebase Ecosystem کار می‌کنند توصیه می‌شود.
  • بدون آپلود dSYM (iOS) و mapping.txt (Android) به سرور کرش، استک‌تریس‌ها unsymbolicated می‌مانند و گزارش‌ها عملا بی‌مصرف هستند.
  • یکپارچه‌سازی این مرحله در پایپ‌لاین dotnet publish و GitHub Actions، نقطه‌ای است که اکثر تیم‌ها رد می‌کنند و بعد در پروداکشن از کرش‌ها بی‌خبر می‌مانند.
  • برای سلامت ریلیز، علاوه بر Sentry باید Crash-Free Sessions، ANR Rate و نرخ adoption نسخه را در داشبورد دنبال کنید.

چرا 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 CrashlyticsBugSnag
SDK رسمی برای .NET MAUIبله (Sentry.Maui)خیر، فقط binding جامعهبله
پشتیبانی از managed .NET exceptionکامل با استک‌تریس C#محدود؛ فقط Native traceکامل
پلتفرم‌های پشتیبانی‌شدهiOS، Android، Windows، Mac Catalyst، TizeniOS و AndroidiOS، Android، Windows
Breadcrumbهای خودکار MAUIبله (lifecycle و UI events)محدودبله
Performance / Tracingبله، با TracesSampleRateFirebase 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 همیشه یک چک‌لیست ثابت است؛ این چک‌لیست تجربه شکست‌های قبلی است.

  1. پکیج Sentry.Maui نسخه پایدار (در زمان نگارش، ۶.۵.۰ و بالاتر) را به پروژه اضافه کنید.
  2. در پنل Sentry، پروژه را با پلتفرم .NET MAUI بسازید و DSN را کپی کنید.
  3. DSN را در appsettings.json یا User Secrets قرار دهید، هرگز در سورس‌کنترل commit نکنید.
  4. در MauiProgram.cs با UseSentry آن را راه‌اندازی کنید و BeforeSend را برای فیلتر PII تنظیم کنید.
  5. یک هندلر سراسری برای AppDomain.CurrentDomain.UnhandledException و TaskScheduler.UnobservedTaskException اضافه کنید.
  6. یک 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

  1. GoogleService-Info.plist برای iOS و google-services.json برای اندروید را به ریشه پروژه اضافه کنید.
  2. روی GoogleService-Info.plist راست‌کلیک کنید و Build Action را BundleResource بگذارید.
  3. برای google-services.json، Build Action را GoogleServicesJson بگذارید.
  4. پکیج 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

پنج اشتباهی که تقریبا در هر پروژه مهاجرت‌شده دیده‌ام:

  1. باقی ماندن پکیج Microsoft.AppCenter.Crashes در csproj: این پکیج هنوز در NuGet موجود است اما عملا کار نمی‌کند. حتما حذفش کنید. در یک پروژه دیدم که دو SDK کرش هم‌زمان نصب بودند و یکی unhandled exception را قبل از دیگری می‌خورد.
  2. نگه داشتن همان user identifier: اگر در App Center با SetUserId کار می‌کردید، در Sentry باید با SentrySdk.ConfigureScope(scope => scope.User = new User { Id = ... }) انجام دهید. این کار را در همان نقطه‌ای انجام دهید که توکن JWT load می‌شود، نه در App.xaml.cs، در غیر این صورت کرش‌های اولین سشن بدون شناسه کاربر می‌آیند.
  3. فراموش کردن آپلود نماد در PRهای hotfix: هر بیلدی که به استور می‌رود باید نمادهایش هم آپلود شده باشد، حتی hotfixهای کوچک. اگر این کار را به دست انسان بسپارید، یک روز کسی فراموش می‌کند و آن دقیقا روزی است که کرش بزرگ رخ می‌دهد.
  4. تنظیم SendDefaultPii = true بدون بررسی GDPR: این flag IP و username را می‌فرستد. در اپلیکیشن‌هایی که در اروپا کار می‌کنند، این می‌تواند نقض GDPR باشد. به‌جایش SendDefaultPii = false بگذارید و فقط شناسه‌های pseudo-anonymous را manual ست کنید.
  5. تست نکردن جریان قبل از انتشار: یک 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های تجمعی هستند که هزینه می‌سازند.

Sofia Rodriguez
درباره نویسنده Sofia Rodriguez

Mobile DevOps engineer focused on the unglamorous stuff: build pipelines, signing, store releases, and the tooling that keeps teams shipping.