otel_auto_route 0.1.0
otel_auto_route: ^0.1.0 copied to clipboard
OpenTelemetry instrumentation for `package:auto_route`. An AutoRouterObserver that emits a span per stack transition AND per tab transition - tabbed apps do not lose views.
otel_auto_route example #
// example/lib/main.dart
import 'package:auto_route/auto_route.dart';
import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';
import 'package:flutter/material.dart';
import 'package:otel_auto_route/otel_auto_route.dart';
part 'main.gr.dart'; // generated by auto_route_generator
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(
page: ShellRoute.page,
initial: true,
children: [
AutoRoute(page: HomeRoute.page, path: 'home'),
AutoRoute(page: SettingsRoute.page, path: 'settings'),
],
),
];
}
Future<void> main() async {
// 1. Bring up OTel before runApp so trace context is already
// flowing when the first route resolves.
await OTel.initialize(
serviceName: 'auto-route-demo',
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final _appRouter = AppRouter();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _appRouter.config(
// 2. A factory, not an instance: every nested router (each
// AutoRouter, each tabs router) calls it again. Returning
// a new observer per navigator is the supported pattern;
// all instances emit through the same tracer.
navigatorObservers: () => [OTelAutoRouterObserver()],
),
);
}
}
// 3. The tabs shell. Tab switches never push or pop a Navigator
// route, so a plain NavigatorObserver misses them entirely —
// OTelAutoRouterObserver receives didInitTabRoute /
// didChangeTabRoute and emits a span per tab view.
@RoutePage()
class ShellPage extends StatelessWidget {
const ShellPage({super.key});
@override
Widget build(BuildContext context) {
return AutoTabsScaffold(
routes: const [HomeRoute(), SettingsRoute()],
bottomNavigationBuilder: (_, tabsRouter) => BottomNavigationBar(
currentIndex: tabsRouter.activeIndex,
onTap: tabsRouter.setActiveIndex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: 'Settings'),
],
),
);
}
}
@RoutePage()
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) =>
const Scaffold(body: Center(child: Text('Home')));
}
@RoutePage()
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) =>
const Scaffold(body: Center(child: Text('Settings')));
}
Trace shape #
(app start — the shell route is pushed onto the stack)
route.push:/ navigation.action=push
navigation.is_initial_route=true
(the first tab activates)
route.tab_init:home navigation.action=tab_init
navigation.route.path=home
(the user taps the Settings tab — no Navigator push happens,
only an AutoRouterObserver sees this)
route.tab_change:settings navigation.action=tab_change
navigation.route.path=settings
navigation.previous_route_path=home
(a detail screen is pushed inside a tab)
route.push:/users/:id navigation.action=push
Span names use the route pattern (/users/:id, not /users/42),
so names stay low-cardinality. Tab spans are zero-length view
markers, ended immediately, matching otel_go_router's model.