Skip to content
Draft
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.25.0+1 # Format: <applicationId>@version+build (matches the build identifier used by Build Distribution)
SENTRY_RELEASE=com.example.empower_flutter@9.28.0+1 # Format: <applicationId>@version+build (matches the build identifier used by Build Distribution)
SENTRY_ENVIRONMENT=your_environment

# ========================================
Expand Down
122 changes: 122 additions & 0 deletions lib/app_start.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// ignore_for_file: experimental_member_use

import 'dart:async';
import 'dart:convert';

import 'package:flutter/services.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

/// Coordinates standalone app-start extension for this demo.
///
/// Call [begin] from `appRunner` *before* `runApp` so `extendAppStart`
/// lands before the first frame. Custom children are parented with
/// `SentryFlutter.getExtendedAppStartSpanV2()` because the demo uses stream
/// lifecycle — `getExtendedAppStartSpan()` is the static-lifecycle getter and
/// returns null here.
///
/// The extension stays open through feature-flag hydration, the first-frame
/// cache write, and the first catalog fetch, then `finishExtendedAppStart()`
/// releases it. If the catalog never arrives, we still finish before the SDK's
/// 30s deadline.
class AppStart {
static const _featureFlagCount = 15;
static const _catalogTimeout = Duration(seconds: 20);

static final Completer<void> _catalogReady = Completer<void>();
static Future<void>? _extension;

/// Must run before the first frame. Safe to call only once.
static void begin() {
if (_extension != null) {
return;
}
SentryFlutter.extendAppStart();
_extension = _run();
unawaited(_extension!);
}

static Future<void> _run() async {
try {
await span(
'Hydrate feature flags',
_hydrateFeatureFlags,
op: 'app.init',
);
await _catalogReady.future.timeout(
_catalogTimeout,
onTimeout: () {},
);
} finally {
await SentryFlutter.finishExtendedAppStart();
}
}

/// Runs [work] as a child of the extended app-start span when one is open.
///
/// In stream mode the extension is not the active span, so we must pass it
/// as `parentSpan`. The callback then becomes the active span (zone), so
/// auto-instrumentation such as `sentry_file` nests under it instead of
/// `ui.load`. If the extension has already ended (or never started —
/// e.g. web/desktop), [work] runs without opening a stray root span.
static Future<T> span<T>(
String name,
Future<T> Function() work, {
String op = 'app.init',
}) async {
final parent = _extendedParent();
if (parent == null) {
return work();
}
return Sentry.startSpan(name, (span) async {
span.setAttribute('sentry.op', SentryAttribute.string(op));
return work();
}, parentSpan: parent);
}

/// Sync counterpart of [span] for first-frame-blocking startup work.
static T spanSync<T>(
String name,
T Function() work, {
String op = 'app.init',
}) {
final parent = _extendedParent();
if (parent == null) {
return work();
}
return Sentry.startSpanSync(name, (span) {
span.setAttribute('sentry.op', SentryAttribute.string(op));
return work();
}, parentSpan: parent);
}

static SentrySpanV2? _extendedParent() =>
SentryFlutter.getExtendedAppStartSpanV2();

/// Signals that the first catalog fetch finished (success or failure).
static void markCatalogReady() {
if (!_catalogReady.isCompleted) {
_catalogReady.complete();
}
}

static Future<void> _hydrateFeatureFlags() async {
final loaded = await Future.wait([
for (var i = 1; i <= _featureFlagCount; i++)
rootBundle.loadString('assets/config/feature_$i.json'),
]);
var enabled = 0;
for (final raw in loaded) {
final decoded = jsonDecode(raw);
if (decoded is Map && decoded['enabled'] == true) {
enabled++;
}
}
Sentry.configureScope((scope) {
scope.setContexts('feature_flags', {
'count': loaded.length,
'enabled': enabled,
});
});
}
}

4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:logging/logging.dart';
import 'backend_config.dart';
import 'platform/platform_info.dart';
import 'app_start.dart';

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final log = Logger('EmpowerPlantLogger');
Expand Down Expand Up @@ -39,6 +40,9 @@ Future<void> main() async {
await initSentry(
appRunner: () {
log.info('Running app');
// Extend before runApp so the first frame cannot close app start.
// Custom children + finish live in AppStart (feature flags + catalog).
AppStart.begin();
runApp(
DefaultAssetBundle(
bundle: SentryAssetBundle(),
Expand Down
26 changes: 19 additions & 7 deletions lib/product_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'platform/platform_info.dart';
import 'platform/file_io_demo.dart';
import 'sentry_setup.dart';
import 'se_config.dart';
import 'app_start.dart';
// ignore: depend_on_referenced_packages

class ItemsList extends StatefulWidget {
Expand All @@ -31,12 +32,16 @@ class _ItemListState extends State<ItemsList> {

Future<ResponseData> fetchShopItems() async {
try {
// Resolve the products endpoint at request time so the OTLP journey
// (which switches BackendConfig.base) targets flask-otlp instead.
final response = await client.get(Uri.parse(BackendConfig.products));
// Simulate full response processing
final data = ResponseData.fromJson((jsonDecode(response.body)));
return data;
// Nest under the extended app-start span while it's still open so the
// first catalog fetch is part of `app.start` (stream mode requires an
// explicit parent — see AppStart.span). After the extension ends this
// is a no-op wrapper and SentryHttpClient still traces the request.
return await AppStart.span('Fetch product catalog', () async {
// Resolve the products endpoint at request time so the OTLP journey
// (which switches BackendConfig.base) targets flask-otlp instead.
final response = await client.get(Uri.parse(BackendConfig.products));
return ResponseData.fromJson((jsonDecode(response.body)));
}, op: 'http.client');
} catch (e) {
rethrow;
}
Expand Down Expand Up @@ -100,6 +105,7 @@ class _ItemListState extends State<ItemsList> {

// Fetch products from API and report TTFD when complete
shopItems = fetchShopItems().whenComplete(() {
AppStart.markCatalogReady();
// Report TTFD as soon as fetching the shop items is done
if (mounted) {
SentryDisplayWidget.of(context).reportFullyDisplayed();
Expand Down Expand Up @@ -127,9 +133,15 @@ class _ItemListState extends State<ItemsList> {

// Simulate slow file I/O on main thread (native: real dart:io file via
// sentry_file; web: equivalent heavy main-thread work — see platform/file_io_demo).
// Nested under extended app start so file.write/file.read attach to `app.start`
// instead of the home `ui.load` (sentry_file parents to the active zone span).
void _performFileIO() {
try {
final size = performFileIODemo();
final size = AppStart.spanSync(
'Warm plant cache',
performFileIODemo,
op: 'file.io',
);

if (kDebugMode) {
print('File I/O on main thread, size: $size');
Expand Down
13 changes: 11 additions & 2 deletions lib/sentry_setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ Future<void> initSentry({required VoidCallback appRunner}) async {
options.dsn = sentryDsn;

// Release must match exactly with uploaded debug symbols
// CRITICAL: This must be in format "appname@version+build" (e.g., "empower_flutter@9.25.0+1")
// CRITICAL: This must be in format "appname@version+build" (e.g., "empower_flutter@9.28.0+1")
options.release = sentryRelease;
options.environment = sentryEnvironment;

// Set distribution to match build number for better symbol matching
options.dist = '1'; // Matches version 9.25.0+1
options.dist = '1'; // Matches version 9.28.0+1

// Debug settings (disabled for production to ensure proper symbol resolution)
// Toggle on via --dart-define=SENTRY_DEBUG=true (or SENTRY_DEBUG=true in .env)
Expand All @@ -104,6 +104,14 @@ Future<void> initSentry({required VoidCallback appRunner}) async {
// the legacy `Sentry.startTransaction` / `startChild` APIs are no-ops here.
options.traceLifecycle = SentryTraceLifecycle.stream;

// Standalone app start (experimental since SDK 9.26.0). Reports process
// start → first frame as its own `App Start` root (`app.start`) instead of
// nesting it under the synthetic initial `ui.load`. Android/iOS only —
// other platforms keep the ui.load-attached behavior. Extended past the
// first frame in `lib/app_start.dart` via extendAppStart / finishExtendedAppStart.
// ignore: experimental_member_use
options.enableStandaloneAppStartTracing = true;

// Enable profiling (relative to tracesSampleRate)
// Required for JSON Decoding, Image Decoding, and Frame Drop detection
// Profiling is available on iOS, macOS, and Android
Expand Down Expand Up @@ -308,6 +316,7 @@ Future<void> initSentry({required VoidCallback appRunner}) async {
'logs',
'metrics',
'distributed_tracing',
'standalone_app_start',
],
});
scope.setContexts('release_info', {
Expand Down
36 changes: 18 additions & 18 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
Expand All @@ -276,10 +276,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
mime:
dependency: transitive
description:
Expand Down Expand Up @@ -372,10 +372,10 @@ packages:
dependency: transitive
description:
name: sentry
sha256: a84bf3a83b3ce1c89fce28b9f97659e3826df14a73a256952465aac2c5efdf5a
sha256: "5641a2acec1647f6482e472679374ac02070464e8c1d0df946361393ea7681a2"
url: "https://pub.dev"
source: hosted
version: "9.25.0"
version: "9.28.0"
sentry_dart_plugin:
dependency: "direct dev"
description:
Expand All @@ -388,34 +388,34 @@ packages:
dependency: "direct main"
description:
name: sentry_dio
sha256: "297caacf2ed037d859611d94165f35c34cf0462420aea1ceb1fed8c87a06f1dd"
sha256: "8d06e18924922e518b7a318c6c22e1069cd356428fd92c7b113f4eb0ea0d7d82"
url: "https://pub.dev"
source: hosted
version: "9.25.0"
version: "9.28.0"
sentry_file:
dependency: "direct main"
description:
name: sentry_file
sha256: "0e4a36c02901855a383972b210ec33fc97a2619d9e0a4f7562538ed2c8f735f1"
sha256: "5f5c1e7748ac0357241cfddd794b24ca9d718ea5e7ae26a080300dd72810dbbd"
url: "https://pub.dev"
source: hosted
version: "9.25.0"
version: "9.28.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: c85575266d91f57364e9b4cb522835156ec3c5581c78339dc7fae491cd0c6f76
sha256: "41df8ab3a30190a05eb44e2c0e739b5ba90eb341de771454ced282295fde8c62"
url: "https://pub.dev"
source: hosted
version: "9.25.0"
version: "9.28.0"
sentry_logging:
dependency: "direct main"
description:
name: sentry_logging
sha256: "7506853de2fe61dd1d814b5266b24360635cd7fc7076c89ea3676f4b4ce2b948"
sha256: "189bfbee55ba4dcb1d68bf32dc8bbd25b2cc2732efa40b801ce5ec36cd18c0c4"
url: "https://pub.dev"
source: hosted
version: "9.25.0"
version: "9.28.0"
sky_engine:
dependency: transitive
description: flutter
Expand Down Expand Up @@ -473,10 +473,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.12"
typed_data:
dependency: transitive
description:
Expand Down Expand Up @@ -561,10 +561,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.4.2"
vm_service:
dependency: transitive
description:
Expand Down
10 changes: 5 additions & 5 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 9.25.0+1
version: 9.28.0+1

environment:
sdk: ">=3.5.0 <4.0.0"
Expand All @@ -36,12 +36,12 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.9
provider: ^6.1.5
sentry_flutter: ^9.25.0
sentry_flutter: ^9.28.0
flutter_dotenv: ^6.0.1
sentry_dio: ^9.25.0
sentry_dio: ^9.28.0
dio: ^5.10.0
sentry_file: ^9.25.0
sentry_logging: ^9.25.0
sentry_file: ^9.28.0
sentry_logging: ^9.28.0
logging: ^1.3.0
# Web View feature (Android/iOS in-app webview; desktop falls back to url_launcher;
# web uses a dart:ui_web iframe via package:web)
Expand Down