diff --git a/.claude/rules/routing.md b/.claude/rules/routing.md
index b8bfdc1..bb4ec98 100644
--- a/.claude/rules/routing.md
+++ b/.claude/rules/routing.md
@@ -17,3 +17,4 @@ path: "lib/src/routing/**/*.dart"
- `LayoutDefinition` — shell route wrapper for persistent navigation (sidebar, bottom nav)
- `RouteServiceProvider` registers routes in boot phase — recommended place for all route definitions
- Custom transitions: `Route.get('/path', () => Page()).transition(TransitionType.fade)`
+- Observer support: `MagicRouter.instance.addObserver(observer)` — must register before `routerConfig` is accessed. Passed to GoRouter `observers` param. Read-only via `observers` getter
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 915a464..3a2d585 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file.
+## [Unreleased]
+
+### ✨ Features
+- **Router Observers**: `MagicRouter.instance.addObserver()` enables NavigatorObserver integration for analytics/monitoring (Sentry, Firebase Analytics, custom observers). Observers are passed to GoRouter automatically. (#31)
+
## [1.0.0-alpha.6] - 2026-04-05
### ✨ Features
diff --git a/doc/basics/routing.md b/doc/basics/routing.md
index 16a0e2c..9fc06a4 100644
--- a/doc/basics/routing.md
+++ b/doc/basics/routing.md
@@ -11,6 +11,7 @@
- [Layouts (Shell Routes)](#layouts-shell-routes)
- [Context-Free Navigation](#context-free-navigation)
- [Route Middleware](#route-middleware)
+- [Navigator Observers](#navigator-observers)
## Introduction
@@ -310,3 +311,37 @@ MagicRoute.page('/admin', () => AdminPanel())
```
See the [Middleware documentation](/basics/middleware) for details on creating custom middleware.
+
+
+## Navigator Observers
+
+Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be added before the router is built (typically in your `RouteServiceProvider`):
+
+```dart
+class RouteServiceProvider extends ServiceProvider {
+ @override
+ Future boot() async {
+ // Add observers before registering routes
+ MagicRouter.instance.addObserver(SentryNavigatorObserver(
+ enableAutoTransactions: true,
+ setRouteNameAsTransaction: true,
+ ));
+
+ MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
+ analytics: FirebaseAnalytics.instance,
+ ));
+
+ registerAppRoutes();
+ }
+
+ void registerAppRoutes() {
+ MagicRoute.page('/', () => HomePage());
+ // ...
+ }
+}
+```
+
+Observers are passed directly to GoRouter and receive all navigation events (`didPush`, `didPop`, `didReplace`, `didRemove`).
+
+> [!NOTE]
+> Observers must be registered before `routerConfig` is accessed. Adding observers after the router is built throws a `StateError`.
diff --git a/lib/src/routing/magic_router.dart b/lib/src/routing/magic_router.dart
index 7701898..0e41950 100644
--- a/lib/src/routing/magic_router.dart
+++ b/lib/src/routing/magic_router.dart
@@ -68,6 +68,9 @@ class MagicRouter {
/// Registered layout definitions.
final List _layouts = [];
+ /// Registered navigator observers.
+ final List _observers = [];
+
/// The built GoRouter instance (lazily created).
GoRouter? _router;
@@ -144,6 +147,22 @@ class MagicRouter {
/// Get all registered routes.
List get routes => List.unmodifiable(_routes);
+ /// Get all registered navigator observers.
+ List get observers => List.unmodifiable(_observers);
+
+ /// Add a navigator observer.
+ ///
+ /// Must be called before the router is built (before [routerConfig] is accessed).
+ void addObserver(NavigatorObserver observer) {
+ if (_isBuilt) {
+ throw StateError(
+ 'Cannot add observers after the router has been built. '
+ 'Register all observers before accessing routerConfig.',
+ );
+ }
+ _observers.add(observer);
+ }
+
// ---------------------------------------------------------------------------
// Router Configuration
// ---------------------------------------------------------------------------
@@ -182,6 +201,7 @@ class MagicRouter {
return GoRouter(
navigatorKey: navigatorKey,
initialLocation: _initialLocation,
+ observers: _observers,
routes: _buildRoutes(),
redirect: _handleRedirect,
onException: (context, state, router) {
@@ -603,6 +623,7 @@ class MagicRouter {
static void reset() {
_instance?._routes.clear();
_instance?._layouts.clear();
+ _instance?._observers.clear();
_instance?._router = null;
_instance?._isBuilt = false;
_instance?._intendedUrl = null;
diff --git a/skills/magic-framework/SKILL.md b/skills/magic-framework/SKILL.md
index 77970b0..427eb9c 100644
--- a/skills/magic-framework/SKILL.md
+++ b/skills/magic-framework/SKILL.md
@@ -620,7 +620,7 @@ Official plugins extending Magic Framework. Each has its own package, service pr
| `references/eloquent-orm.md` | Model definition, attributes, casts, relations, `InteractsWithPersistence`, QueryBuilder, migrations, Blueprint | Working with models, database queries, or migrations |
| `references/controllers-views.md` | MagicController, MagicStateMixin, RxStatus, MagicView, MagicStatefulView, MagicStatefulViewState, MagicResponsiveView, MagicBuilder, MagicCan/MagicCannot | Building controllers or views, reactive state, authorization widgets |
| `references/forms-validation.md` | MagicFormData, MagicForm, rules(), FormValidator, ValidatesRequests, built-in rules (Required, Email, Min, Max, Confirmed, Same, Accepted), process(), processingListenable | Building forms, adding validation, handling server-side errors |
-| `references/routing-navigation.md` | MagicRoute.page(), group(), layout(), navigation (to/back/replace/push/toNamed), middleware, transitions, MagicRouterOutlet, path/query parameters | Defining routes, navigation, or middleware |
+| `references/routing-navigation.md` | MagicRoute.page(), group(), layout(), navigation (to/back/replace/push/toNamed), middleware, transitions, MagicRouterOutlet, path/query parameters, navigator observers | Defining routes, navigation, middleware, or observers |
| `references/http-network.md` | Http facade (get/post/put/delete/upload + RESTful resource methods), MagicResponse API, interceptors, network config | Making HTTP requests, handling responses, or configuring network layer |
| `references/auth-system.md` | Auth facade, AuthManager, guards (Bearer, BasicAuth, ApiKey), token refresh, setUserFactory, restore, policies, Gate, MagicCan | Implementing authentication, authorization, or token management |
| `references/secondary-systems.md` | Cache, Events (EventDispatcher, register listeners), Logging, Localization (trans()), Storage, Encryption, Vault, Carbon date helper, Launch, Policies | Using caching, events, logging, i18n, file storage, encryption, or URL launching |
diff --git a/skills/magic-framework/references/routing-navigation.md b/skills/magic-framework/references/routing-navigation.md
index 24fcf25..572d42f 100644
--- a/skills/magic-framework/references/routing-navigation.md
+++ b/skills/magic-framework/references/routing-navigation.md
@@ -420,8 +420,33 @@ MagicRoute.layout(
);
```
+## Navigator Observers
+
+Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be registered before the router is built.
+
+```dart
+// In RouteServiceProvider.boot()
+MagicRouter.instance.addObserver(SentryNavigatorObserver(
+ enableAutoTransactions: true,
+ setRouteNameAsTransaction: true,
+));
+
+MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
+ analytics: FirebaseAnalytics.instance,
+));
+```
+
+Read-only access to registered observers:
+
+```dart
+final observers = MagicRouter.instance.observers; // List (unmodifiable)
+```
+
+Observers are passed to GoRouter's `observers` parameter automatically. Adding observers after `routerConfig` is accessed throws `StateError`.
+
## Gotchas
+- **Observer Registration Timing:** Observers must be added before `routerConfig` is accessed, same as routes. Register in `RouteServiceProvider.boot()`.
- **Route Registration Timing:** Routes must be registered during `ServiceProvider.register()` or `boot()`. They cannot be added after `MagicRouter.instance.routerConfig` is accessed.
- **Middleware Next Required:** Middleware must call `next()` to allow the request to proceed. Failing to call it halts the pipeline.
- **Path Parameters:** Parameters are injected by position into the handler function. Ensure the function signature matches the number of parameters in the route.
diff --git a/test/routing/router_test.dart b/test/routing/router_test.dart
index a2a2f80..2225d22 100644
--- a/test/routing/router_test.dart
+++ b/test/routing/router_test.dart
@@ -345,6 +345,50 @@ void main() {
});
});
+ group('Observer Support', () {
+ test('addObserver() stores observer', () {
+ final observer = _TestNavigatorObserver();
+
+ MagicRouter.instance.addObserver(observer);
+
+ expect(MagicRouter.instance.observers, contains(observer));
+ });
+
+ test('observers persist after router build', () {
+ final observer = _TestNavigatorObserver();
+
+ MagicRouter.instance.addObserver(observer);
+ MagicRoute.page('/', () => const SizedBox());
+
+ final config = MagicRouter.instance.routerConfig;
+
+ expect(config, isNotNull);
+ expect(MagicRouter.instance.observers, contains(observer));
+ });
+
+ test('addObserver() throws after build', () {
+ MagicRoute.page('/', () => const SizedBox());
+
+ // Trigger the lazy build.
+ MagicRouter.instance.routerConfig;
+
+ expect(
+ () => MagicRouter.instance.addObserver(_TestNavigatorObserver()),
+ throwsA(isA()),
+ );
+ });
+
+ test('reset() clears observers', () {
+ final observer = _TestNavigatorObserver();
+
+ MagicRouter.instance.addObserver(observer);
+
+ MagicRouter.reset();
+
+ expect(MagicRouter.instance.observers, isEmpty);
+ });
+ });
+
/// History-based back() navigation.
///
/// These tests verify the history tracking feature, including
@@ -590,3 +634,6 @@ class _TestMiddleware extends MagicMiddleware {
@override
Future handle(void Function() next) async => next();
}
+
+/// Test NavigatorObserver implementation.
+class _TestNavigatorObserver extends NavigatorObserver {}