Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# ========================================
SENTRY_AUTH_TOKEN=your_auth_token
SENTRY_DSN=your_dsn
SENTRY_RELEASE=com.example.empower_flutter@9.22.0+1 # Format: <applicationId>@version+build (matches the build identifier used by Build Distribution)
SENTRY_RELEASE=com.example.empower_flutter@9.25.0+1 # Format: <applicationId>@version+build (matches the build identifier used by Build Distribution)
SENTRY_ENVIRONMENT=your_environment

# ========================================
Expand Down
71 changes: 47 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Project Overview

**Application:** Empower Plant (`empower_flutter`)
**Version:** 9.22.0+1 (matches Sentry SDK version)
**Version:** 9.25.0+1 (matches Sentry SDK version)
**Purpose:** Production-ready Flutter e-commerce app with comprehensive Sentry instrumentation demonstrating best practices for error monitoring, performance tracking, session replay, and user feedback.

**Type:** Full-featured plant shopping app + Sentry demo platform
Expand All @@ -24,7 +24,7 @@
- Sentry config: DSN, ORG, PROJECT, AUTH_TOKEN required

### Version Management
- **Format:** `package@version+build` (e.g., `com.example.empower_flutter@9.22.0+1`)
- **Format:** `package@version+build` (e.g., `com.example.empower_flutter@9.25.0+1`)
- Version source: `pubspec.yaml`
- Must match Sentry SDK version for consistency
- Distribution set to build number (currently `'1'`)
Expand Down Expand Up @@ -88,19 +88,19 @@ Flutter SDK: >= 3.22.0 < 4.0.0
Dart SDK: >= 3.5.0 < 4.0.0

# Sentry
sentry_flutter: ^9.22.0 # Main SDK
sentry_dio: ^9.22.0 # HTTP client integration
sentry_file: ^9.22.0 # File I/O tracking
sentry_logging: ^9.22.0 # Logging integration
sentry_flutter: ^9.25.0 # Main SDK
sentry_dio: ^9.25.0 # HTTP client integration
sentry_file: ^9.25.0 # File I/O tracking
sentry_logging: ^9.25.0 # Logging integration

# State Management & Utils
provider: ^6.1.5 # State management
flutter_dotenv: ^6.0.0 # Environment variables
dio: ^5.9.1 # HTTP client
flutter_dotenv: ^6.0.1 # Environment variables
dio: ^5.10.0 # HTTP client
logging: ^1.3.0 # Structured logging

# Web View feature
webview_flutter: ^4.13.0 # In-app WebView (Android/iOS)
webview_flutter: ^4.14.1 # In-app WebView (Android/iOS)
url_launcher: ^6.3.1 # Desktop fallback (system browser)
web: ^1.1.1 # Flutter web iframe support
```
Expand Down Expand Up @@ -130,6 +130,12 @@ sampleRate: 1.0 # All errors captured
tracesSampleRate: 1.0 # All performance traces
profilesSampleRate: 1.0 # All profiling (iOS/macOS/Android)

// Span Streaming API (stable since SDK 9.23.0)
traceLifecycle: SentryTraceLifecycle.stream # Send each span as it finishes
// NOTE: mutually-exclusive switch. In `stream` mode manual spans MUST use the
// `Sentry.startSpan` / `startSpanSync` callback API; the legacy
// `Sentry.startTransaction` / `startChild` APIs are no-ops.

// Session Replay (100% for demo)
replay.onErrorSampleRate: 1.0 # All error sessions
replay.sessionSampleRate: 1.0 # All normal sessions
Expand Down Expand Up @@ -182,6 +188,7 @@ tracePropagationTargets: [ # Backends that receive trace headers
**Session Replay Privacy Masking:**
- Real prices are shown in full in the app UI (e.g. cart item lines and subtotal show `$155.00`); they are masked only in Session Replay
- The `maskCallback` masks Text widgets containing a `$…` value so financial values stay private in replays
- Image masking is explicitly disabled (`maskAllImages = false`, `maskAssetImages = false`) so product photos, icons, and badges render normally in replays — otherwise every image blanks into a redaction box and the replay looks broken
- Everything else in replays is visible

### Integrations Enabled
Expand Down Expand Up @@ -238,16 +245,32 @@ try {
```

### Performance Instrumentation
```dart
// Transaction
final transaction = Sentry.startTransaction('operation_name', 'operation_type');

// Span
final span = transaction.startChild('child_operation', description: 'Details');
// ... work ...
await span.finish();
Uses the Span Streaming API (`traceLifecycle = SentryTraceLifecycle.stream`).
Manual spans use the `Sentry.startSpan` callback API — the span ends automatically
when the callback returns, and its status is set to `error` if the callback throws.
The legacy `Sentry.startTransaction` / `startChild` APIs are no-ops in stream mode.

await transaction.finish(status: SpanStatus.ok());
```dart
// Root span (own trace): pass parentSpan: null. Nested startSpan calls
// automatically parent to the active span via zones.
await Sentry.startSpan('operation_name', (span) async {
span.setAttribute('sentry.op', SentryAttribute.string('operation_type'));
span.setAttribute('key', SentryAttribute.string('value'));

await Sentry.startSpan('child_operation', (child) async {
child.setAttribute('sentry.op', SentryAttribute.string('child_type'));
// ... work ...
// On failure inside a caught block: child.status = SentrySpanStatusV2.error;
});
}, parentSpan: null);

// For a span that must outlive a single callback (widget lifecycle, stream
// subscription, platform round-trip): startInactiveSpan + manual end().
final span = Sentry.startInactiveSpan('name', parentSpan: null);
// ... later ...
span.status = SentrySpanStatusV2.ok;
span.end();
```

### Structured Logging
Expand Down Expand Up @@ -359,7 +382,7 @@ await client.get(Uri.parse('https://example.com'));
```bash
SENTRY_AUTH_TOKEN=sntryu_xxx # Sentry auth token
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
SENTRY_RELEASE=com.example.empower_flutter@9.22.0+1
SENTRY_RELEASE=com.example.empower_flutter@9.25.0+1
SENTRY_ENVIRONMENT=development # development/staging/production
SENTRY_ORG=your-org-slug
SENTRY_PROJECT=your-project-slug
Expand Down Expand Up @@ -484,9 +507,9 @@ flutter analyze
4. Use `Sentry.captureException()` to capture

**Add Performance Instrumentation:**
1. Start transaction: `Sentry.startTransaction()`
2. Add spans for sub-operations
3. Finish with appropriate status
1. Start a root span: `await Sentry.startSpan('name', (span) async {...}, parentSpan: null)`
2. Add nested `Sentry.startSpan(...)` calls for sub-operations (auto-parented via zones)
3. Set the op with `span.setAttribute('sentry.op', SentryAttribute.string('...'))`; status is automatic (error on throw)
4. Add metrics if needed

---
Expand Down Expand Up @@ -580,9 +603,9 @@ SENTRY_SIZE_ANALYSIS_ENABLED=true
- **GitHub (Sentry SDK):** `https://github.com/getsentry/sentry-dart`

### Important Version Numbers
- **App Version:** 9.22.0+1
- **App Version:** 9.25.0+1
- **Flutter SDK:** >= 3.22.0
- **Sentry SDK:** ^9.22.0
- **Sentry SDK:** ^9.25.0

### File Locations
- **Config:** `.env`, `lib/se_config.dart`, `pubspec.yaml`
Expand All @@ -597,4 +620,4 @@ SENTRY_SIZE_ANALYSIS_ENABLED=true

*Last Updated: Session creating this CLAUDE.md*
*Current Branch: feature/comprehensive-sentry-integration*
*App Version: 9.22.0+1*
*App Version: 9.25.0+1*
26 changes: 26 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("io.sentry.android.gradle")
}

android {
Expand Down Expand Up @@ -43,3 +44,28 @@ android {
flutter {
source = "../.."
}

// Sentry Android Gradle plugin — scoped to ProGuard/R8 mapping handling only.
// It embeds a per-build ProGuard UUID into the APK and uploads the matching
// mapping during the release Gradle build, so Sentry can de-obfuscate DEX for
// both crash reports and the size-analysis breakdown. Org/project/token are
// read from the environment (demo.sh exports them via `.env`); when they're
// absent (e.g. a plain local build) the upload is simply skipped.
sentry {
System.getenv("SENTRY_ORG")?.let { org.set(it) }
System.getenv("SENTRY_PROJECT")?.let { projectName.set(it) }
System.getenv("SENTRY_AUTH_TOKEN")?.let { authToken.set(it) }

// Embed the mapping UUID into the APK and upload the mapping keyed by it.
includeProguardMapping.set(true)
autoUploadProguardMapping.set(true)

// This app uses the sentry_flutter SDK, so the AGP must NOT add its own
// native SDK or instrument bytecode (avoids duplicate/conflicting setup).
autoInstallation {
enabled.set(false)
}
tracingInstrumentation {
enabled.set(false)
}
}
3 changes: 3 additions & 0 deletions android/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
// Embeds the R8/ProGuard UUID into the APK and uploads the matching mapping
// so Sentry can de-obfuscate DEX (crash reports + size-analysis breakdown).
id("io.sentry.android.gradle") version "6.15.0" apply false
}

include(":app")
23 changes: 8 additions & 15 deletions demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -480,22 +480,15 @@ build_windows() {
print_success "Windows app built: build/windows/x64/runner/Release/"
}

# Upload ProGuard mapping for Android builds (must be called before size analysis)
# ProGuard/R8 mapping upload is handled by the Sentry Android Gradle plugin
# (android/app/build.gradle.kts), NOT here. The plugin embeds a per-build UUID
# into the APK and uploads the matching mapping during the Gradle build, so
# Sentry can de-obfuscate DEX for crash reports AND the size-analysis breakdown.
# Uploading the mapping manually here (via `sentry-cli upload-proguard`) keyed it
# by the file's own UUID, which the APK didn't reference — leaving it orphaned
# ("Missing proguard mapping" in size analysis). This is now a no-op.
upload_proguard_mapping() {
local proguard_mapping="build/app/outputs/mapping/release/mapping.txt"
if [ -f "$proguard_mapping" ]; then
print_info "Uploading ProGuard mapping for detailed DEX breakdown..."
if check_sentry_cli; then
if sentry-cli upload-proguard --org "${SENTRY_ORG}" --project "${SENTRY_PROJECT}" "$proguard_mapping" > /tmp/proguard_upload.log 2>&1; then
print_success "ProGuard mapping uploaded ($(du -h "$proguard_mapping" | cut -f1))"
else
print_warning "ProGuard mapping upload failed"
cat /tmp/proguard_upload.log
fi
fi
else
print_info "ProGuard mapping not found, skipping"
fi
print_info "ProGuard mapping upload handled by the Sentry Android Gradle plugin (during build)"
}

# Upload debug symbols to Sentry
Expand Down
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ PODS:
- package_info_plus (0.4.5):
- Flutter
- Sentry/HybridSDK (8.58.3)
- sentry_flutter (9.22.0):
- sentry_flutter (9.24.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.58.3)
Expand Down Expand Up @@ -40,7 +40,7 @@ SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
Sentry: 108fdbb76299c4189af12246bf0308c09c278922
sentry_flutter: fbb8a76a5a009ce7ba7bde295ee6b50b11916851
sentry_flutter: 8528594bf73819ef3f40c2e218e93554fc2719e6
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d

Expand Down
39 changes: 18 additions & 21 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,26 +248,23 @@ class _HomePageState extends State<HomePage> {

Future<void> instrumentedOperation(
String name,
Future<void> Function(ISentrySpan span) operation,
Future<void> Function(SentrySpanV2 span) operation,
) async {
final transaction = Sentry.startTransaction(
name,
'custom',
bindToScope: true,
);
// Example custom performance metrics
transaction.setMeasurement('memoryUsed', 123);
transaction.setMeasurement('ui.footerComponent.render', 1.3);
transaction.setMeasurement('localStorageRead', 4);
try {
await operation(transaction);
} catch (exception, stackTrace) {
transaction.throwable = exception;
transaction.status = SpanStatus.internalError();
await Sentry.captureException(exception, stackTrace: stackTrace);
} finally {
await transaction.finish();
}
await Sentry.startSpan(name, (span) async {
span.setAttribute('sentry.op', SentryAttribute.string('custom'));
// Example custom performance measurements. `setMeasurement` was a v1
// transaction-only concept; in the span-streaming API these are recorded
// as span attributes.
span.setAttribute('measurement.memoryUsed', SentryAttribute.int(123));
span.setAttribute('measurement.ui.footerComponent.render', SentryAttribute.double(1.3));
span.setAttribute('measurement.localStorageRead', SentryAttribute.int(4));
try {
await operation(span);
} catch (exception, stackTrace) {
span.status = SentrySpanStatusV2.error;
await Sentry.captureException(exception, stackTrace: stackTrace);
}
}, parentSpan: null);
}

// Example usage for widget build instrumentation
Expand All @@ -279,8 +276,8 @@ class InstrumentedDestinationView extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: instrumentedOperation('build_${destination.title}', (span) async {
span.setData('destination_icon', destination.icon.toString());
span.setData('destination_title', destination.title);
span.setAttribute('destination_icon', SentryAttribute.string(destination.icon.toString()));
span.setAttribute('destination_title', SentryAttribute.string(destination.title));
// Simulate build work
await Future.delayed(const Duration(milliseconds: 10));
}),
Expand Down
Loading
Loading