From 30895ed0c7463d0f3e8bc19f3f32696594c24aa2 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 5 Apr 2026 21:49:04 +0300 Subject: [PATCH 1/2] feat(routing): add NavigatorObserver support to MagicRouter (#31) MagicRouter.instance.addObserver() enables Sentry, Firebase Analytics, and custom NavigatorObserver integration. Observers are passed to GoRouter automatically. Registration enforces same pre-build guard as routes. --- .claude/rules/routing.md | 1 + CHANGELOG.md | 5 ++ doc/basics/routing.md | 35 ++++++++++++++ lib/src/routing/magic_router.dart | 21 +++++++++ skills/magic-framework/SKILL.md | 2 +- .../references/routing-navigation.md | 25 ++++++++++ test/routing/router_test.dart | 47 +++++++++++++++++++ 7 files changed, 135 insertions(+), 1 deletion(-) 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..b1a6d60 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 are passed to GoRouter', () { + 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 {} From d72b37056e8ccd931427ce7ceb11bb1353a6cbd2 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 5 Apr 2026 22:04:47 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(test):=20rename=20misleading=20observer?= =?UTF-8?q?=20test=20=E2=80=94=20GoRouter=20doesn't=20expose=20observers?= =?UTF-8?q?=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed 'observers are passed to GoRouter' → 'observers persist after router build' since GoRouter doesn't publicly expose its observer list. --- test/routing/router_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/routing/router_test.dart b/test/routing/router_test.dart index b1a6d60..2225d22 100644 --- a/test/routing/router_test.dart +++ b/test/routing/router_test.dart @@ -354,7 +354,7 @@ void main() { expect(MagicRouter.instance.observers, contains(observer)); }); - test('observers are passed to GoRouter', () { + test('observers persist after router build', () { final observer = _TestNavigatorObserver(); MagicRouter.instance.addObserver(observer);