@truewatchtech/mobile-react-navigation 0.0.0-stage → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,102 @@
1
- # Temporary Holding Version
1
+ # TrueWatch React Navigation plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This plugin tracks React Navigation route changes as TrueWatch RUM Views and
4
+ measures native-stack transition loading.
5
+
6
+ ## Installation
7
+
8
+ ```sh
9
+ yarn add @truewatchtech/react-native-mobile @truewatchtech/mobile-react-navigation
10
+ ```
11
+
12
+ Keep your existing React Navigation dependencies. This package contains only
13
+ JavaScript and types; it adds no native linking or build configuration.
14
+ Initialize the mobile SDK and RUM before enabling View tracking.
15
+
16
+ ## Route tracking
17
+
18
+ Use this mode for route changes, including nested navigators and tabs. It records
19
+ the current leaf route when the container is ready, then listens for changes.
20
+ It does not invent a loading duration when no transition boundary is available.
21
+
22
+ ```tsx
23
+ import {
24
+ NavigationContainer,
25
+ createNavigationContainerRef,
26
+ } from '@react-navigation/native';
27
+ import { FTRumReactNavigationTracking } from '@truewatchtech/mobile-react-navigation';
28
+
29
+ const navigationRef = createNavigationContainerRef<RootStackParamList>();
30
+
31
+ <NavigationContainer
32
+ ref={navigationRef}
33
+ onReady={() => FTRumReactNavigationTracking.startTrackingViews(navigationRef)}
34
+ >
35
+ {/* navigators */}
36
+ </NavigationContainer>;
37
+
38
+ // Before disposing this navigation container or shutting down the SDK:
39
+ FTRumReactNavigationTracking.stopTrackingViews(navigationRef);
40
+ ```
41
+
42
+ Calling `startTrackingViews` repeatedly with the same ref registers once.
43
+ Registering another ref disposes the previous registration. A cleanup using an
44
+ old ref cannot stop the new registration.
45
+
46
+ ## Transition loading
47
+
48
+ Choose this mode instead of `startTrackingViews` when using
49
+ `@react-navigation/native-stack`. Attach the listeners to the stack that owns the
50
+ screen transitions:
51
+
52
+ ```tsx
53
+ import { FTRumReactNavigationTracking } from '@truewatchtech/mobile-react-navigation';
54
+
55
+ <Stack.Navigator screenListeners={FTRumReactNavigationTracking.StackListener}>
56
+ {/* screens */}
57
+ </Stack.Navigator>;
58
+
59
+ // Before disposing the tracked stack or shutting down the SDK:
60
+ FTRumReactNavigationTracking.stopTrackingViews();
61
+ ```
62
+
63
+ Views start when the selected route changes, including the initial screen and
64
+ screens without animations. Loading is recorded only for a route instance's
65
+ first appearance, from its first observed `focus` to the matching opening
66
+ `transitionEnd`. The interval uses a monotonic clock and is reported in
67
+ nanoseconds; for example, 500 ms is 500,000,000 ns. Repeated focus events do not
68
+ replace the original start, and `transitionStart` is not required.
69
+
70
+ Returning to an existing route, changing tabs, and foreground recovery create
71
+ Views without repeating loading measurements. An outer stack's transition is
72
+ never assigned to a nested leaf screen. Missing, cancelled, backgrounded, or
73
+ invalid intervals are discarded. Loading is applied only while the same View
74
+ appearance remains active.
75
+
76
+ This measures the interval between JavaScript focus and transition-end callbacks. It does not
77
+ measure network completion or when all screen content is ready, and native-to-JS
78
+ event delivery delays can affect the observed interval.
79
+
80
+ Use one mode on one root container/owning stack at a time. Do not attach separate
81
+ timing listeners to every level of a nested navigator. Starting route tracking
82
+ switches off stack tracking, and acquiring `StackListener` switches to stack
83
+ tracking. After disposal, acquire `StackListener` again when mounting a new stack;
84
+ callbacks retained from the disposed registration are ignored.
85
+
86
+ ## Lifecycle
87
+
88
+ Both modes use the SDK's shared automatic View lifecycle. Native handles closing
89
+ Views in the background. Foreground recovery starts the current screen once
90
+ without reusing old loading measurements. Route names become View names; route
91
+ keys distinguish separate appearances of screens with the same name.
92
+
93
+ ## Migration
94
+
95
+ Replace the copied `example/src/FTRumReactNavigationTracking.tsx` import with
96
+ `@truewatchtech/mobile-react-navigation`, retaining the class name and methods.
97
+ Remove the copied implementation so it cannot collect the same Views twice. Applications
98
+ using only this plugin do not need `react-native-navigation` or its TrueWatch plugin.
99
+
100
+ ## License
101
+
102
+ Apache 2.0
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.FTRumReactNavigationTracking = void 0;
7
+ var _reactNativeMobile = require("@truewatchtech/react-native-mobile");
8
+ var _FTRumReactNavigationTracking;
9
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
10
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
11
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
12
+ const MAX_TRANSITION_MS = 15 * 60 * 1000;
13
+ /** One active navigation container, using either route or stack tracking. */
14
+ class FTRumReactNavigationTracking {
15
+ /** Call from NavigationContainer.onReady. This mode does not measure loading. */
16
+ static startTrackingViews(ref) {
17
+ if (!ref || this.registeredContainer === ref) return;
18
+ this.stopTrackingViews();
19
+ this.registeredContainer = ref;
20
+ this.tracker.start();
21
+ const lifecycle = this.lifecycle;
22
+ this.unsubscribe = ref.addListener('state', () => {
23
+ if (lifecycle !== this.lifecycle) return;
24
+ const route = ref.getCurrentRoute();
25
+ if (route) this.show(route);
26
+ });
27
+ const route = ref.getCurrentRoute();
28
+ if (route) this.show(route);
29
+ }
30
+
31
+ /** Omit ref to dispose stack tracking. Old containers cannot stop a new one. */
32
+ static stopTrackingViews(ref) {
33
+ var _this$unsubscribe;
34
+ if (ref !== undefined && ref !== this.registeredContainer) return;
35
+ ++this.lifecycle;
36
+ (_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 || _this$unsubscribe.call(this);
37
+ this.unsubscribe = undefined;
38
+ this.registeredContainer = null;
39
+ this.stackListeners = undefined;
40
+ this.tracker.stop();
41
+ this.visited.clear();
42
+ this.appearance = undefined;
43
+ this.transition = undefined;
44
+ this.trackRouter = undefined;
45
+ }
46
+
47
+ /** Track Views on state changes; time first focus through opening transitionEnd. */
48
+ static get StackListener() {
49
+ if (this.stackListeners) return this.stackListeners;
50
+ this.stopTrackingViews();
51
+ const lifecycle = this.lifecycle;
52
+ const current = () => lifecycle === this.lifecycle;
53
+ this.stackListeners = {
54
+ transitionStart: event => {
55
+ var _event$data;
56
+ if (current() && event.target && ((_event$data = event.data) === null || _event$data === void 0 ? void 0 : _event$data.closing) === true) this.cancelTransition(event.target);
57
+ },
58
+ transitionEnd: event => {
59
+ var _event$data2, _event$data3, _this$transition, _this$appearance;
60
+ const end = this.monotonicNow();
61
+ if (!current() || !event.target) return;
62
+ if (((_event$data2 = event.data) === null || _event$data2 === void 0 ? void 0 : _event$data2.closing) === true) {
63
+ this.cancelTransition(event.target);
64
+ return;
65
+ }
66
+ if (((_event$data3 = event.data) === null || _event$data3 === void 0 ? void 0 : _event$data3.closing) !== false) {
67
+ this.cancelTransition(event.target);
68
+ return;
69
+ }
70
+ if (((_this$transition = this.transition) === null || _this$transition === void 0 ? void 0 : _this$transition.target) === event.target) {
71
+ // Capture the end now, even when the matching state arrives later.
72
+ if (this.transition.end === undefined) this.transition.end = end;
73
+ this.finishTransition();
74
+ } else if (((_this$appearance = this.appearance) === null || _this$appearance === void 0 ? void 0 : _this$appearance.target) === event.target) {
75
+ // No observed focus: do not infer a start from a later callback.
76
+ this.appearance.first = false;
77
+ }
78
+ },
79
+ gestureCancel: () => {
80
+ if (!current()) return;
81
+ this.transition = undefined;
82
+ if (this.appearance) this.appearance.first = false;
83
+ },
84
+ state: event => {
85
+ var _event$data4, _state$index;
86
+ if (!current()) return;
87
+ const state = (_event$data4 = event.data) === null || _event$data4 === void 0 ? void 0 : _event$data4.state;
88
+ if (!state) return;
89
+ const outer = state.routes[(_state$index = state.index) !== null && _state$index !== void 0 ? _state$index : 0];
90
+ const leaf = this.leafRoute(outer);
91
+ if (!outer || !leaf) {
92
+ this.tracker.stop();
93
+ this.visited.clear();
94
+ this.appearance = undefined;
95
+ this.transition = undefined;
96
+ this.trackRouter = undefined;
97
+ return;
98
+ }
99
+ // Retain instance history only while its route remains in this stack.
100
+ const keys = new Set(state.routes.map(route => route.key));
101
+ for (const key of this.visited) {
102
+ if (!keys.has(key)) this.visited.delete(key);
103
+ }
104
+ if (this.transition && this.transition.target !== outer.key) this.transition = undefined;
105
+ const previous = this.appearance;
106
+ if ((previous === null || previous === void 0 ? void 0 : previous.target) !== outer.key || previous.route.key !== leaf.key) {
107
+ const route = {
108
+ key: leaf.key,
109
+ name: leaf.name
110
+ };
111
+ const first = !this.visited.has(outer.key) && outer.key === leaf.key;
112
+ this.visited.add(outer.key);
113
+ this.appearance = {
114
+ target: outer.key,
115
+ route,
116
+ first,
117
+ generation: this.show(route)
118
+ };
119
+ } else {
120
+ this.tracker.setCurrent(previous.route);
121
+ }
122
+ this.finishTransition();
123
+ },
124
+ focus: event => {
125
+ var _this$transition3;
126
+ const start = this.monotonicNow();
127
+ if (!current() || !event.target) return;
128
+ this.tracker.start();
129
+ const appearance = this.appearance;
130
+ // A newly selected route invalidates the previous appearance even if
131
+ // React Navigation has not delivered the matching state update yet.
132
+ if ((appearance === null || appearance === void 0 ? void 0 : appearance.target) !== event.target) {
133
+ var _this$transition2;
134
+ if (appearance) appearance.first = false;
135
+ if (((_this$transition2 = this.transition) === null || _this$transition2 === void 0 ? void 0 : _this$transition2.target) !== event.target) this.transition = undefined;
136
+ }
137
+ // Keep the first focus boundary. Existing instances (including nested
138
+ // containers) may open Views again, but never start another load timer.
139
+ if (this.visited.has(event.target) && ((appearance === null || appearance === void 0 ? void 0 : appearance.target) !== event.target || !appearance.first) || ((_this$transition3 = this.transition) === null || _this$transition3 === void 0 ? void 0 : _this$transition3.target) === event.target) return;
140
+ this.transition = {
141
+ target: event.target,
142
+ start
143
+ };
144
+ }
145
+ };
146
+ return this.stackListeners;
147
+ }
148
+ static finishTransition() {
149
+ const transition = this.transition;
150
+ const appearance = this.appearance;
151
+ if (!transition || transition.end === undefined || !appearance || appearance.target !== transition.target) return;
152
+ this.transition = undefined;
153
+ const eligible = appearance.first;
154
+ appearance.first = false;
155
+ const elapsed = transition.end - transition.start;
156
+ if (!eligible || !Number.isFinite(elapsed) || elapsed <= 0 || elapsed >= MAX_TRANSITION_MS) return;
157
+ this.tracker.updateLoadingTime(appearance.route.key, appearance.generation, Math.floor(elapsed * 1000000));
158
+ }
159
+ static cancelTransition(target) {
160
+ var _this$transition4, _this$appearance2;
161
+ if (((_this$transition4 = this.transition) === null || _this$transition4 === void 0 ? void 0 : _this$transition4.target) === target) this.transition = undefined;
162
+ if (((_this$appearance2 = this.appearance) === null || _this$appearance2 === void 0 ? void 0 : _this$appearance2.target) === target) this.appearance.first = false;
163
+ }
164
+ static monotonicNow() {
165
+ return typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : NaN;
166
+ }
167
+ static leafRoute(route) {
168
+ while ((_route = route) !== null && _route !== void 0 && _route.state) {
169
+ var _route, _route$state$index;
170
+ const child = route.state.routes[(_route$state$index = route.state.index) !== null && _route$state$index !== void 0 ? _route$state$index : 0];
171
+ if (!child) break;
172
+ route = child;
173
+ }
174
+ return route;
175
+ }
176
+ static show(route) {
177
+ this.trackRouter = route;
178
+ return this.tracker.show(route);
179
+ }
180
+ }
181
+ exports.FTRumReactNavigationTracking = FTRumReactNavigationTracking;
182
+ _FTRumReactNavigationTracking = FTRumReactNavigationTracking;
183
+ _defineProperty(FTRumReactNavigationTracking, "tracker", new _reactNativeMobile.FTRumViewTracking(() => {
184
+ _FTRumReactNavigationTracking.transition = undefined;
185
+ if (_FTRumReactNavigationTracking.appearance) _FTRumReactNavigationTracking.appearance.first = false;
186
+ }));
187
+ _defineProperty(FTRumReactNavigationTracking, "registeredContainer", null);
188
+ _defineProperty(FTRumReactNavigationTracking, "unsubscribe", void 0);
189
+ _defineProperty(FTRumReactNavigationTracking, "lifecycle", 0);
190
+ _defineProperty(FTRumReactNavigationTracking, "stackListeners", void 0);
191
+ _defineProperty(FTRumReactNavigationTracking, "visited", new Set());
192
+ _defineProperty(FTRumReactNavigationTracking, "appearance", void 0);
193
+ _defineProperty(FTRumReactNavigationTracking, "transition", void 0);
194
+ _defineProperty(FTRumReactNavigationTracking, "trackRouter", void 0);
195
+ //# sourceMappingURL=FTRumReactNavigationTracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNativeMobile","require","_FTRumReactNavigationTracking","_defineProperty","e","r","t","_toPropertyKey","Object","defineProperty","value","enumerable","configurable","writable","i","_toPrimitive","Symbol","toPrimitive","call","TypeError","String","Number","MAX_TRANSITION_MS","FTRumReactNavigationTracking","startTrackingViews","ref","registeredContainer","stopTrackingViews","tracker","start","lifecycle","unsubscribe","addListener","route","getCurrentRoute","show","_this$unsubscribe","undefined","stackListeners","stop","visited","clear","appearance","transition","trackRouter","StackListener","current","transitionStart","event","_event$data","target","data","closing","cancelTransition","transitionEnd","_event$data2","_event$data3","_this$transition","_this$appearance","end","monotonicNow","finishTransition","first","gestureCancel","state","_event$data4","_state$index","outer","routes","index","leaf","leafRoute","keys","Set","map","key","has","delete","previous","name","add","generation","setCurrent","focus","_this$transition3","_this$transition2","eligible","elapsed","isFinite","updateLoadingTime","Math","floor","_this$transition4","_this$appearance2","performance","now","NaN","_route","_route$state$index","child","exports","FTRumViewTracking"],"sources":["FTRumReactNavigationTracking.ts"],"sourcesContent":["import type {\n NavigationContainerRef,\n ParamListBase,\n} from '@react-navigation/native';\nimport { FTRumViewTracking } from '@truewatchtech/react-native-mobile';\n\ntype TrackedRoute = {\n key: string;\n name: string;\n state?: { index?: number; routes: readonly TrackedRoute[] };\n};\ntype StackEvent = {\n target?: string;\n data?: {\n closing?: boolean;\n state?: { index?: number; routes: readonly TrackedRoute[] };\n };\n};\ntype Appearance = {\n target: string;\n route: { key: string; name: string };\n generation?: number;\n first: boolean;\n};\ntype Transition = { target: string; start: number; end?: number };\nconst MAX_TRANSITION_MS = 15 * 60 * 1000;\n\nexport type FTRumStackListeners = {\n focus(event: StackEvent): void;\n state(event: StackEvent): void;\n transitionStart(event: StackEvent): void;\n transitionEnd(event: StackEvent): void;\n gestureCancel(event: StackEvent): void;\n};\n\n/** One active navigation container, using either route or stack tracking. */\nexport class FTRumReactNavigationTracking {\n private static tracker = new FTRumViewTracking(() => {\n this.transition = undefined;\n if (this.appearance) this.appearance.first = false;\n });\n private static registeredContainer: object | null = null;\n private static unsubscribe?: () => void;\n private static lifecycle = 0;\n private static stackListeners?: FTRumStackListeners;\n private static visited = new Set<string>();\n private static appearance?: Appearance;\n private static transition?: Transition;\n static trackRouter: TrackedRoute | undefined;\n\n /** Call from NavigationContainer.onReady. This mode does not measure loading. */\n static startTrackingViews<ParamList extends ParamListBase>(\n ref: NavigationContainerRef<ParamList> | null\n ): void {\n if (!ref || this.registeredContainer === ref) return;\n this.stopTrackingViews();\n this.registeredContainer = ref;\n this.tracker.start();\n const lifecycle = this.lifecycle;\n this.unsubscribe = ref.addListener('state', () => {\n if (lifecycle !== this.lifecycle) return;\n const route = ref.getCurrentRoute();\n if (route) this.show(route);\n });\n const route = ref.getCurrentRoute();\n if (route) this.show(route);\n }\n\n /** Omit ref to dispose stack tracking. Old containers cannot stop a new one. */\n static stopTrackingViews(ref?: object | null): void {\n if (ref !== undefined && ref !== this.registeredContainer) return;\n ++this.lifecycle;\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n this.registeredContainer = null;\n this.stackListeners = undefined;\n this.tracker.stop();\n this.visited.clear();\n this.appearance = undefined;\n this.transition = undefined;\n this.trackRouter = undefined;\n }\n\n /** Track Views on state changes; time first focus through opening transitionEnd. */\n static get StackListener(): FTRumStackListeners {\n if (this.stackListeners) return this.stackListeners;\n this.stopTrackingViews();\n const lifecycle = this.lifecycle;\n const current = (): boolean => lifecycle === this.lifecycle;\n this.stackListeners = {\n transitionStart: (event): void => {\n if (current() && event.target && event.data?.closing === true)\n this.cancelTransition(event.target);\n },\n transitionEnd: (event): void => {\n const end = this.monotonicNow();\n if (!current() || !event.target) return;\n if (event.data?.closing === true) {\n this.cancelTransition(event.target);\n return;\n }\n if (event.data?.closing !== false) {\n this.cancelTransition(event.target);\n return;\n }\n if (this.transition?.target === event.target) {\n // Capture the end now, even when the matching state arrives later.\n if (this.transition.end === undefined) this.transition.end = end;\n this.finishTransition();\n } else if (this.appearance?.target === event.target) {\n // No observed focus: do not infer a start from a later callback.\n this.appearance.first = false;\n }\n },\n gestureCancel: (): void => {\n if (!current()) return;\n this.transition = undefined;\n if (this.appearance) this.appearance.first = false;\n },\n state: (event): void => {\n if (!current()) return;\n const state = event.data?.state;\n if (!state) return;\n const outer = state.routes[state.index ?? 0];\n const leaf = this.leafRoute(outer);\n if (!outer || !leaf) {\n this.tracker.stop();\n this.visited.clear();\n this.appearance = undefined;\n this.transition = undefined;\n this.trackRouter = undefined;\n return;\n }\n // Retain instance history only while its route remains in this stack.\n const keys = new Set(state.routes.map((route) => route.key));\n for (const key of this.visited) {\n if (!keys.has(key)) this.visited.delete(key);\n }\n if (this.transition && this.transition.target !== outer.key)\n this.transition = undefined;\n const previous = this.appearance;\n if (previous?.target !== outer.key || previous.route.key !== leaf.key) {\n const route = { key: leaf.key, name: leaf.name };\n const first = !this.visited.has(outer.key) && outer.key === leaf.key;\n this.visited.add(outer.key);\n this.appearance = {\n target: outer.key,\n route,\n first,\n generation: this.show(route),\n };\n } else {\n this.tracker.setCurrent(previous.route);\n }\n this.finishTransition();\n },\n focus: (event): void => {\n const start = this.monotonicNow();\n if (!current() || !event.target) return;\n this.tracker.start();\n const appearance = this.appearance;\n // A newly selected route invalidates the previous appearance even if\n // React Navigation has not delivered the matching state update yet.\n if (appearance?.target !== event.target) {\n if (appearance) appearance.first = false;\n if (this.transition?.target !== event.target)\n this.transition = undefined;\n }\n // Keep the first focus boundary. Existing instances (including nested\n // containers) may open Views again, but never start another load timer.\n if (\n (this.visited.has(event.target) &&\n (appearance?.target !== event.target || !appearance.first)) ||\n this.transition?.target === event.target\n )\n return;\n this.transition = { target: event.target, start };\n },\n };\n return this.stackListeners;\n }\n\n private static finishTransition(): void {\n const transition = this.transition;\n const appearance = this.appearance;\n if (\n !transition ||\n transition.end === undefined ||\n !appearance ||\n appearance.target !== transition.target\n )\n return;\n this.transition = undefined;\n const eligible = appearance.first;\n appearance.first = false;\n const elapsed = transition.end - transition.start;\n if (\n !eligible ||\n !Number.isFinite(elapsed) ||\n elapsed <= 0 ||\n elapsed >= MAX_TRANSITION_MS\n )\n return;\n this.tracker.updateLoadingTime(\n appearance.route.key,\n appearance.generation,\n Math.floor(elapsed * 1000000)\n );\n }\n\n private static cancelTransition(target: string): void {\n if (this.transition?.target === target) this.transition = undefined;\n if (this.appearance?.target === target) this.appearance.first = false;\n }\n\n private static monotonicNow(): number {\n return typeof performance !== 'undefined' &&\n typeof performance.now === 'function'\n ? performance.now()\n : NaN;\n }\n\n private static leafRoute(\n route: TrackedRoute | undefined\n ): TrackedRoute | undefined {\n while (route?.state) {\n const child = route.state.routes[route.state.index ?? 0];\n if (!child) break;\n route = child;\n }\n return route;\n }\n\n private static show(route: TrackedRoute): number | undefined {\n this.trackRouter = route;\n return this.tracker.show(route);\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "FTRumReactNavigationTracking", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _FTRumReactNavigationTracking.FTRumReactNavigationTracking;
10
+ }
11
+ });
12
+ var _FTRumReactNavigationTracking = require("./FTRumReactNavigationTracking");
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_FTRumReactNavigationTracking","require"],"sources":["index.ts"],"sourcesContent":["export { FTRumReactNavigationTracking } from './FTRumReactNavigationTracking';\nexport type { FTRumStackListeners } from './FTRumReactNavigationTracking';\n"],"mappings":";;;;;;;;;;;AAAA,IAAAA,6BAAA,GAAAC,OAAA","ignoreList":[]}
@@ -0,0 +1,188 @@
1
+ var _FTRumReactNavigationTracking;
2
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
3
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
4
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
5
+ import { FTRumViewTracking } from '@truewatchtech/react-native-mobile';
6
+ const MAX_TRANSITION_MS = 15 * 60 * 1000;
7
+ /** One active navigation container, using either route or stack tracking. */
8
+ export class FTRumReactNavigationTracking {
9
+ /** Call from NavigationContainer.onReady. This mode does not measure loading. */
10
+ static startTrackingViews(ref) {
11
+ if (!ref || this.registeredContainer === ref) return;
12
+ this.stopTrackingViews();
13
+ this.registeredContainer = ref;
14
+ this.tracker.start();
15
+ const lifecycle = this.lifecycle;
16
+ this.unsubscribe = ref.addListener('state', () => {
17
+ if (lifecycle !== this.lifecycle) return;
18
+ const route = ref.getCurrentRoute();
19
+ if (route) this.show(route);
20
+ });
21
+ const route = ref.getCurrentRoute();
22
+ if (route) this.show(route);
23
+ }
24
+
25
+ /** Omit ref to dispose stack tracking. Old containers cannot stop a new one. */
26
+ static stopTrackingViews(ref) {
27
+ var _this$unsubscribe;
28
+ if (ref !== undefined && ref !== this.registeredContainer) return;
29
+ ++this.lifecycle;
30
+ (_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 || _this$unsubscribe.call(this);
31
+ this.unsubscribe = undefined;
32
+ this.registeredContainer = null;
33
+ this.stackListeners = undefined;
34
+ this.tracker.stop();
35
+ this.visited.clear();
36
+ this.appearance = undefined;
37
+ this.transition = undefined;
38
+ this.trackRouter = undefined;
39
+ }
40
+
41
+ /** Track Views on state changes; time first focus through opening transitionEnd. */
42
+ static get StackListener() {
43
+ if (this.stackListeners) return this.stackListeners;
44
+ this.stopTrackingViews();
45
+ const lifecycle = this.lifecycle;
46
+ const current = () => lifecycle === this.lifecycle;
47
+ this.stackListeners = {
48
+ transitionStart: event => {
49
+ var _event$data;
50
+ if (current() && event.target && ((_event$data = event.data) === null || _event$data === void 0 ? void 0 : _event$data.closing) === true) this.cancelTransition(event.target);
51
+ },
52
+ transitionEnd: event => {
53
+ var _event$data2, _event$data3, _this$transition, _this$appearance;
54
+ const end = this.monotonicNow();
55
+ if (!current() || !event.target) return;
56
+ if (((_event$data2 = event.data) === null || _event$data2 === void 0 ? void 0 : _event$data2.closing) === true) {
57
+ this.cancelTransition(event.target);
58
+ return;
59
+ }
60
+ if (((_event$data3 = event.data) === null || _event$data3 === void 0 ? void 0 : _event$data3.closing) !== false) {
61
+ this.cancelTransition(event.target);
62
+ return;
63
+ }
64
+ if (((_this$transition = this.transition) === null || _this$transition === void 0 ? void 0 : _this$transition.target) === event.target) {
65
+ // Capture the end now, even when the matching state arrives later.
66
+ if (this.transition.end === undefined) this.transition.end = end;
67
+ this.finishTransition();
68
+ } else if (((_this$appearance = this.appearance) === null || _this$appearance === void 0 ? void 0 : _this$appearance.target) === event.target) {
69
+ // No observed focus: do not infer a start from a later callback.
70
+ this.appearance.first = false;
71
+ }
72
+ },
73
+ gestureCancel: () => {
74
+ if (!current()) return;
75
+ this.transition = undefined;
76
+ if (this.appearance) this.appearance.first = false;
77
+ },
78
+ state: event => {
79
+ var _event$data4, _state$index;
80
+ if (!current()) return;
81
+ const state = (_event$data4 = event.data) === null || _event$data4 === void 0 ? void 0 : _event$data4.state;
82
+ if (!state) return;
83
+ const outer = state.routes[(_state$index = state.index) !== null && _state$index !== void 0 ? _state$index : 0];
84
+ const leaf = this.leafRoute(outer);
85
+ if (!outer || !leaf) {
86
+ this.tracker.stop();
87
+ this.visited.clear();
88
+ this.appearance = undefined;
89
+ this.transition = undefined;
90
+ this.trackRouter = undefined;
91
+ return;
92
+ }
93
+ // Retain instance history only while its route remains in this stack.
94
+ const keys = new Set(state.routes.map(route => route.key));
95
+ for (const key of this.visited) {
96
+ if (!keys.has(key)) this.visited.delete(key);
97
+ }
98
+ if (this.transition && this.transition.target !== outer.key) this.transition = undefined;
99
+ const previous = this.appearance;
100
+ if ((previous === null || previous === void 0 ? void 0 : previous.target) !== outer.key || previous.route.key !== leaf.key) {
101
+ const route = {
102
+ key: leaf.key,
103
+ name: leaf.name
104
+ };
105
+ const first = !this.visited.has(outer.key) && outer.key === leaf.key;
106
+ this.visited.add(outer.key);
107
+ this.appearance = {
108
+ target: outer.key,
109
+ route,
110
+ first,
111
+ generation: this.show(route)
112
+ };
113
+ } else {
114
+ this.tracker.setCurrent(previous.route);
115
+ }
116
+ this.finishTransition();
117
+ },
118
+ focus: event => {
119
+ var _this$transition3;
120
+ const start = this.monotonicNow();
121
+ if (!current() || !event.target) return;
122
+ this.tracker.start();
123
+ const appearance = this.appearance;
124
+ // A newly selected route invalidates the previous appearance even if
125
+ // React Navigation has not delivered the matching state update yet.
126
+ if ((appearance === null || appearance === void 0 ? void 0 : appearance.target) !== event.target) {
127
+ var _this$transition2;
128
+ if (appearance) appearance.first = false;
129
+ if (((_this$transition2 = this.transition) === null || _this$transition2 === void 0 ? void 0 : _this$transition2.target) !== event.target) this.transition = undefined;
130
+ }
131
+ // Keep the first focus boundary. Existing instances (including nested
132
+ // containers) may open Views again, but never start another load timer.
133
+ if (this.visited.has(event.target) && ((appearance === null || appearance === void 0 ? void 0 : appearance.target) !== event.target || !appearance.first) || ((_this$transition3 = this.transition) === null || _this$transition3 === void 0 ? void 0 : _this$transition3.target) === event.target) return;
134
+ this.transition = {
135
+ target: event.target,
136
+ start
137
+ };
138
+ }
139
+ };
140
+ return this.stackListeners;
141
+ }
142
+ static finishTransition() {
143
+ const transition = this.transition;
144
+ const appearance = this.appearance;
145
+ if (!transition || transition.end === undefined || !appearance || appearance.target !== transition.target) return;
146
+ this.transition = undefined;
147
+ const eligible = appearance.first;
148
+ appearance.first = false;
149
+ const elapsed = transition.end - transition.start;
150
+ if (!eligible || !Number.isFinite(elapsed) || elapsed <= 0 || elapsed >= MAX_TRANSITION_MS) return;
151
+ this.tracker.updateLoadingTime(appearance.route.key, appearance.generation, Math.floor(elapsed * 1000000));
152
+ }
153
+ static cancelTransition(target) {
154
+ var _this$transition4, _this$appearance2;
155
+ if (((_this$transition4 = this.transition) === null || _this$transition4 === void 0 ? void 0 : _this$transition4.target) === target) this.transition = undefined;
156
+ if (((_this$appearance2 = this.appearance) === null || _this$appearance2 === void 0 ? void 0 : _this$appearance2.target) === target) this.appearance.first = false;
157
+ }
158
+ static monotonicNow() {
159
+ return typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : NaN;
160
+ }
161
+ static leafRoute(route) {
162
+ while ((_route = route) !== null && _route !== void 0 && _route.state) {
163
+ var _route, _route$state$index;
164
+ const child = route.state.routes[(_route$state$index = route.state.index) !== null && _route$state$index !== void 0 ? _route$state$index : 0];
165
+ if (!child) break;
166
+ route = child;
167
+ }
168
+ return route;
169
+ }
170
+ static show(route) {
171
+ this.trackRouter = route;
172
+ return this.tracker.show(route);
173
+ }
174
+ }
175
+ _FTRumReactNavigationTracking = FTRumReactNavigationTracking;
176
+ _defineProperty(FTRumReactNavigationTracking, "tracker", new FTRumViewTracking(() => {
177
+ _FTRumReactNavigationTracking.transition = undefined;
178
+ if (_FTRumReactNavigationTracking.appearance) _FTRumReactNavigationTracking.appearance.first = false;
179
+ }));
180
+ _defineProperty(FTRumReactNavigationTracking, "registeredContainer", null);
181
+ _defineProperty(FTRumReactNavigationTracking, "unsubscribe", void 0);
182
+ _defineProperty(FTRumReactNavigationTracking, "lifecycle", 0);
183
+ _defineProperty(FTRumReactNavigationTracking, "stackListeners", void 0);
184
+ _defineProperty(FTRumReactNavigationTracking, "visited", new Set());
185
+ _defineProperty(FTRumReactNavigationTracking, "appearance", void 0);
186
+ _defineProperty(FTRumReactNavigationTracking, "transition", void 0);
187
+ _defineProperty(FTRumReactNavigationTracking, "trackRouter", void 0);
188
+ //# sourceMappingURL=FTRumReactNavigationTracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["FTRumViewTracking","MAX_TRANSITION_MS","FTRumReactNavigationTracking","startTrackingViews","ref","registeredContainer","stopTrackingViews","tracker","start","lifecycle","unsubscribe","addListener","route","getCurrentRoute","show","_this$unsubscribe","undefined","call","stackListeners","stop","visited","clear","appearance","transition","trackRouter","StackListener","current","transitionStart","event","_event$data","target","data","closing","cancelTransition","transitionEnd","_event$data2","_event$data3","_this$transition","_this$appearance","end","monotonicNow","finishTransition","first","gestureCancel","state","_event$data4","_state$index","outer","routes","index","leaf","leafRoute","keys","Set","map","key","has","delete","previous","name","add","generation","setCurrent","focus","_this$transition3","_this$transition2","eligible","elapsed","Number","isFinite","updateLoadingTime","Math","floor","_this$transition4","_this$appearance2","performance","now","NaN","_route","_route$state$index","child","_FTRumReactNavigationTracking","_defineProperty"],"sources":["FTRumReactNavigationTracking.ts"],"sourcesContent":["import type {\n NavigationContainerRef,\n ParamListBase,\n} from '@react-navigation/native';\nimport { FTRumViewTracking } from '@truewatchtech/react-native-mobile';\n\ntype TrackedRoute = {\n key: string;\n name: string;\n state?: { index?: number; routes: readonly TrackedRoute[] };\n};\ntype StackEvent = {\n target?: string;\n data?: {\n closing?: boolean;\n state?: { index?: number; routes: readonly TrackedRoute[] };\n };\n};\ntype Appearance = {\n target: string;\n route: { key: string; name: string };\n generation?: number;\n first: boolean;\n};\ntype Transition = { target: string; start: number; end?: number };\nconst MAX_TRANSITION_MS = 15 * 60 * 1000;\n\nexport type FTRumStackListeners = {\n focus(event: StackEvent): void;\n state(event: StackEvent): void;\n transitionStart(event: StackEvent): void;\n transitionEnd(event: StackEvent): void;\n gestureCancel(event: StackEvent): void;\n};\n\n/** One active navigation container, using either route or stack tracking. */\nexport class FTRumReactNavigationTracking {\n private static tracker = new FTRumViewTracking(() => {\n this.transition = undefined;\n if (this.appearance) this.appearance.first = false;\n });\n private static registeredContainer: object | null = null;\n private static unsubscribe?: () => void;\n private static lifecycle = 0;\n private static stackListeners?: FTRumStackListeners;\n private static visited = new Set<string>();\n private static appearance?: Appearance;\n private static transition?: Transition;\n static trackRouter: TrackedRoute | undefined;\n\n /** Call from NavigationContainer.onReady. This mode does not measure loading. */\n static startTrackingViews<ParamList extends ParamListBase>(\n ref: NavigationContainerRef<ParamList> | null\n ): void {\n if (!ref || this.registeredContainer === ref) return;\n this.stopTrackingViews();\n this.registeredContainer = ref;\n this.tracker.start();\n const lifecycle = this.lifecycle;\n this.unsubscribe = ref.addListener('state', () => {\n if (lifecycle !== this.lifecycle) return;\n const route = ref.getCurrentRoute();\n if (route) this.show(route);\n });\n const route = ref.getCurrentRoute();\n if (route) this.show(route);\n }\n\n /** Omit ref to dispose stack tracking. Old containers cannot stop a new one. */\n static stopTrackingViews(ref?: object | null): void {\n if (ref !== undefined && ref !== this.registeredContainer) return;\n ++this.lifecycle;\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n this.registeredContainer = null;\n this.stackListeners = undefined;\n this.tracker.stop();\n this.visited.clear();\n this.appearance = undefined;\n this.transition = undefined;\n this.trackRouter = undefined;\n }\n\n /** Track Views on state changes; time first focus through opening transitionEnd. */\n static get StackListener(): FTRumStackListeners {\n if (this.stackListeners) return this.stackListeners;\n this.stopTrackingViews();\n const lifecycle = this.lifecycle;\n const current = (): boolean => lifecycle === this.lifecycle;\n this.stackListeners = {\n transitionStart: (event): void => {\n if (current() && event.target && event.data?.closing === true)\n this.cancelTransition(event.target);\n },\n transitionEnd: (event): void => {\n const end = this.monotonicNow();\n if (!current() || !event.target) return;\n if (event.data?.closing === true) {\n this.cancelTransition(event.target);\n return;\n }\n if (event.data?.closing !== false) {\n this.cancelTransition(event.target);\n return;\n }\n if (this.transition?.target === event.target) {\n // Capture the end now, even when the matching state arrives later.\n if (this.transition.end === undefined) this.transition.end = end;\n this.finishTransition();\n } else if (this.appearance?.target === event.target) {\n // No observed focus: do not infer a start from a later callback.\n this.appearance.first = false;\n }\n },\n gestureCancel: (): void => {\n if (!current()) return;\n this.transition = undefined;\n if (this.appearance) this.appearance.first = false;\n },\n state: (event): void => {\n if (!current()) return;\n const state = event.data?.state;\n if (!state) return;\n const outer = state.routes[state.index ?? 0];\n const leaf = this.leafRoute(outer);\n if (!outer || !leaf) {\n this.tracker.stop();\n this.visited.clear();\n this.appearance = undefined;\n this.transition = undefined;\n this.trackRouter = undefined;\n return;\n }\n // Retain instance history only while its route remains in this stack.\n const keys = new Set(state.routes.map((route) => route.key));\n for (const key of this.visited) {\n if (!keys.has(key)) this.visited.delete(key);\n }\n if (this.transition && this.transition.target !== outer.key)\n this.transition = undefined;\n const previous = this.appearance;\n if (previous?.target !== outer.key || previous.route.key !== leaf.key) {\n const route = { key: leaf.key, name: leaf.name };\n const first = !this.visited.has(outer.key) && outer.key === leaf.key;\n this.visited.add(outer.key);\n this.appearance = {\n target: outer.key,\n route,\n first,\n generation: this.show(route),\n };\n } else {\n this.tracker.setCurrent(previous.route);\n }\n this.finishTransition();\n },\n focus: (event): void => {\n const start = this.monotonicNow();\n if (!current() || !event.target) return;\n this.tracker.start();\n const appearance = this.appearance;\n // A newly selected route invalidates the previous appearance even if\n // React Navigation has not delivered the matching state update yet.\n if (appearance?.target !== event.target) {\n if (appearance) appearance.first = false;\n if (this.transition?.target !== event.target)\n this.transition = undefined;\n }\n // Keep the first focus boundary. Existing instances (including nested\n // containers) may open Views again, but never start another load timer.\n if (\n (this.visited.has(event.target) &&\n (appearance?.target !== event.target || !appearance.first)) ||\n this.transition?.target === event.target\n )\n return;\n this.transition = { target: event.target, start };\n },\n };\n return this.stackListeners;\n }\n\n private static finishTransition(): void {\n const transition = this.transition;\n const appearance = this.appearance;\n if (\n !transition ||\n transition.end === undefined ||\n !appearance ||\n appearance.target !== transition.target\n )\n return;\n this.transition = undefined;\n const eligible = appearance.first;\n appearance.first = false;\n const elapsed = transition.end - transition.start;\n if (\n !eligible ||\n !Number.isFinite(elapsed) ||\n elapsed <= 0 ||\n elapsed >= MAX_TRANSITION_MS\n )\n return;\n this.tracker.updateLoadingTime(\n appearance.route.key,\n appearance.generation,\n Math.floor(elapsed * 1000000)\n );\n }\n\n private static cancelTransition(target: string): void {\n if (this.transition?.target === target) this.transition = undefined;\n if (this.appearance?.target === target) this.appearance.first = false;\n }\n\n private static monotonicNow(): number {\n return typeof performance !== 'undefined' &&\n typeof performance.now === 'function'\n ? performance.now()\n : NaN;\n }\n\n private static leafRoute(\n route: TrackedRoute | undefined\n ): TrackedRoute | undefined {\n while (route?.state) {\n const child = route.state.routes[route.state.index ?? 0];\n if (!child) break;\n route = child;\n }\n return route;\n }\n\n private static show(route: TrackedRoute): number | undefined {\n this.trackRouter = route;\n return this.tracker.show(route);\n }\n}\n"],"mappings":";;;;AAIA,SAASA,iBAAiB,QAAQ,oCAAoC;AAqBtE,MAAMC,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;AAUxC;AACA,OAAO,MAAMC,4BAA4B,CAAC;EAcxC;EACA,OAAOC,kBAAkBA,CACvBC,GAA6C,EACvC;IACN,IAAI,CAACA,GAAG,IAAI,IAAI,CAACC,mBAAmB,KAAKD,GAAG,EAAE;IAC9C,IAAI,CAACE,iBAAiB,CAAC,CAAC;IACxB,IAAI,CAACD,mBAAmB,GAAGD,GAAG;IAC9B,IAAI,CAACG,OAAO,CAACC,KAAK,CAAC,CAAC;IACpB,MAAMC,SAAS,GAAG,IAAI,CAACA,SAAS;IAChC,IAAI,CAACC,WAAW,GAAGN,GAAG,CAACO,WAAW,CAAC,OAAO,EAAE,MAAM;MAChD,IAAIF,SAAS,KAAK,IAAI,CAACA,SAAS,EAAE;MAClC,MAAMG,KAAK,GAAGR,GAAG,CAACS,eAAe,CAAC,CAAC;MACnC,IAAID,KAAK,EAAE,IAAI,CAACE,IAAI,CAACF,KAAK,CAAC;IAC7B,CAAC,CAAC;IACF,MAAMA,KAAK,GAAGR,GAAG,CAACS,eAAe,CAAC,CAAC;IACnC,IAAID,KAAK,EAAE,IAAI,CAACE,IAAI,CAACF,KAAK,CAAC;EAC7B;;EAEA;EACA,OAAON,iBAAiBA,CAACF,GAAmB,EAAQ;IAAA,IAAAW,iBAAA;IAClD,IAAIX,GAAG,KAAKY,SAAS,IAAIZ,GAAG,KAAK,IAAI,CAACC,mBAAmB,EAAE;IAC3D,EAAE,IAAI,CAACI,SAAS;IAChB,CAAAM,iBAAA,OAAI,CAACL,WAAW,cAAAK,iBAAA,eAAhBA,iBAAA,CAAAE,IAAA,KAAmB,CAAC;IACpB,IAAI,CAACP,WAAW,GAAGM,SAAS;IAC5B,IAAI,CAACX,mBAAmB,GAAG,IAAI;IAC/B,IAAI,CAACa,cAAc,GAAGF,SAAS;IAC/B,IAAI,CAACT,OAAO,CAACY,IAAI,CAAC,CAAC;IACnB,IAAI,CAACC,OAAO,CAACC,KAAK,CAAC,CAAC;IACpB,IAAI,CAACC,UAAU,GAAGN,SAAS;IAC3B,IAAI,CAACO,UAAU,GAAGP,SAAS;IAC3B,IAAI,CAACQ,WAAW,GAAGR,SAAS;EAC9B;;EAEA;EACA,WAAWS,aAAaA,CAAA,EAAwB;IAC9C,IAAI,IAAI,CAACP,cAAc,EAAE,OAAO,IAAI,CAACA,cAAc;IACnD,IAAI,CAACZ,iBAAiB,CAAC,CAAC;IACxB,MAAMG,SAAS,GAAG,IAAI,CAACA,SAAS;IAChC,MAAMiB,OAAO,GAAGA,CAAA,KAAejB,SAAS,KAAK,IAAI,CAACA,SAAS;IAC3D,IAAI,CAACS,cAAc,GAAG;MACpBS,eAAe,EAAGC,KAAK,IAAW;QAAA,IAAAC,WAAA;QAChC,IAAIH,OAAO,CAAC,CAAC,IAAIE,KAAK,CAACE,MAAM,IAAI,EAAAD,WAAA,GAAAD,KAAK,CAACG,IAAI,cAAAF,WAAA,uBAAVA,WAAA,CAAYG,OAAO,MAAK,IAAI,EAC3D,IAAI,CAACC,gBAAgB,CAACL,KAAK,CAACE,MAAM,CAAC;MACvC,CAAC;MACDI,aAAa,EAAGN,KAAK,IAAW;QAAA,IAAAO,YAAA,EAAAC,YAAA,EAAAC,gBAAA,EAAAC,gBAAA;QAC9B,MAAMC,GAAG,GAAG,IAAI,CAACC,YAAY,CAAC,CAAC;QAC/B,IAAI,CAACd,OAAO,CAAC,CAAC,IAAI,CAACE,KAAK,CAACE,MAAM,EAAE;QACjC,IAAI,EAAAK,YAAA,GAAAP,KAAK,CAACG,IAAI,cAAAI,YAAA,uBAAVA,YAAA,CAAYH,OAAO,MAAK,IAAI,EAAE;UAChC,IAAI,CAACC,gBAAgB,CAACL,KAAK,CAACE,MAAM,CAAC;UACnC;QACF;QACA,IAAI,EAAAM,YAAA,GAAAR,KAAK,CAACG,IAAI,cAAAK,YAAA,uBAAVA,YAAA,CAAYJ,OAAO,MAAK,KAAK,EAAE;UACjC,IAAI,CAACC,gBAAgB,CAACL,KAAK,CAACE,MAAM,CAAC;UACnC;QACF;QACA,IAAI,EAAAO,gBAAA,OAAI,CAACd,UAAU,cAAAc,gBAAA,uBAAfA,gBAAA,CAAiBP,MAAM,MAAKF,KAAK,CAACE,MAAM,EAAE;UAC5C;UACA,IAAI,IAAI,CAACP,UAAU,CAACgB,GAAG,KAAKvB,SAAS,EAAE,IAAI,CAACO,UAAU,CAACgB,GAAG,GAAGA,GAAG;UAChE,IAAI,CAACE,gBAAgB,CAAC,CAAC;QACzB,CAAC,MAAM,IAAI,EAAAH,gBAAA,OAAI,CAAChB,UAAU,cAAAgB,gBAAA,uBAAfA,gBAAA,CAAiBR,MAAM,MAAKF,KAAK,CAACE,MAAM,EAAE;UACnD;UACA,IAAI,CAACR,UAAU,CAACoB,KAAK,GAAG,KAAK;QAC/B;MACF,CAAC;MACDC,aAAa,EAAEA,CAAA,KAAY;QACzB,IAAI,CAACjB,OAAO,CAAC,CAAC,EAAE;QAChB,IAAI,CAACH,UAAU,GAAGP,SAAS;QAC3B,IAAI,IAAI,CAACM,UAAU,EAAE,IAAI,CAACA,UAAU,CAACoB,KAAK,GAAG,KAAK;MACpD,CAAC;MACDE,KAAK,EAAGhB,KAAK,IAAW;QAAA,IAAAiB,YAAA,EAAAC,YAAA;QACtB,IAAI,CAACpB,OAAO,CAAC,CAAC,EAAE;QAChB,MAAMkB,KAAK,IAAAC,YAAA,GAAGjB,KAAK,CAACG,IAAI,cAAAc,YAAA,uBAAVA,YAAA,CAAYD,KAAK;QAC/B,IAAI,CAACA,KAAK,EAAE;QACZ,MAAMG,KAAK,GAAGH,KAAK,CAACI,MAAM,EAAAF,YAAA,GAACF,KAAK,CAACK,KAAK,cAAAH,YAAA,cAAAA,YAAA,GAAI,CAAC,CAAC;QAC5C,MAAMI,IAAI,GAAG,IAAI,CAACC,SAAS,CAACJ,KAAK,CAAC;QAClC,IAAI,CAACA,KAAK,IAAI,CAACG,IAAI,EAAE;UACnB,IAAI,CAAC3C,OAAO,CAACY,IAAI,CAAC,CAAC;UACnB,IAAI,CAACC,OAAO,CAACC,KAAK,CAAC,CAAC;UACpB,IAAI,CAACC,UAAU,GAAGN,SAAS;UAC3B,IAAI,CAACO,UAAU,GAAGP,SAAS;UAC3B,IAAI,CAACQ,WAAW,GAAGR,SAAS;UAC5B;QACF;QACA;QACA,MAAMoC,IAAI,GAAG,IAAIC,GAAG,CAACT,KAAK,CAACI,MAAM,CAACM,GAAG,CAAE1C,KAAK,IAAKA,KAAK,CAAC2C,GAAG,CAAC,CAAC;QAC5D,KAAK,MAAMA,GAAG,IAAI,IAAI,CAACnC,OAAO,EAAE;UAC9B,IAAI,CAACgC,IAAI,CAACI,GAAG,CAACD,GAAG,CAAC,EAAE,IAAI,CAACnC,OAAO,CAACqC,MAAM,CAACF,GAAG,CAAC;QAC9C;QACA,IAAI,IAAI,CAAChC,UAAU,IAAI,IAAI,CAACA,UAAU,CAACO,MAAM,KAAKiB,KAAK,CAACQ,GAAG,EACzD,IAAI,CAAChC,UAAU,GAAGP,SAAS;QAC7B,MAAM0C,QAAQ,GAAG,IAAI,CAACpC,UAAU;QAChC,IAAI,CAAAoC,QAAQ,aAARA,QAAQ,uBAARA,QAAQ,CAAE5B,MAAM,MAAKiB,KAAK,CAACQ,GAAG,IAAIG,QAAQ,CAAC9C,KAAK,CAAC2C,GAAG,KAAKL,IAAI,CAACK,GAAG,EAAE;UACrE,MAAM3C,KAAK,GAAG;YAAE2C,GAAG,EAAEL,IAAI,CAACK,GAAG;YAAEI,IAAI,EAAET,IAAI,CAACS;UAAK,CAAC;UAChD,MAAMjB,KAAK,GAAG,CAAC,IAAI,CAACtB,OAAO,CAACoC,GAAG,CAACT,KAAK,CAACQ,GAAG,CAAC,IAAIR,KAAK,CAACQ,GAAG,KAAKL,IAAI,CAACK,GAAG;UACpE,IAAI,CAACnC,OAAO,CAACwC,GAAG,CAACb,KAAK,CAACQ,GAAG,CAAC;UAC3B,IAAI,CAACjC,UAAU,GAAG;YAChBQ,MAAM,EAAEiB,KAAK,CAACQ,GAAG;YACjB3C,KAAK;YACL8B,KAAK;YACLmB,UAAU,EAAE,IAAI,CAAC/C,IAAI,CAACF,KAAK;UAC7B,CAAC;QACH,CAAC,MAAM;UACL,IAAI,CAACL,OAAO,CAACuD,UAAU,CAACJ,QAAQ,CAAC9C,KAAK,CAAC;QACzC;QACA,IAAI,CAAC6B,gBAAgB,CAAC,CAAC;MACzB,CAAC;MACDsB,KAAK,EAAGnC,KAAK,IAAW;QAAA,IAAAoC,iBAAA;QACtB,MAAMxD,KAAK,GAAG,IAAI,CAACgC,YAAY,CAAC,CAAC;QACjC,IAAI,CAACd,OAAO,CAAC,CAAC,IAAI,CAACE,KAAK,CAACE,MAAM,EAAE;QACjC,IAAI,CAACvB,OAAO,CAACC,KAAK,CAAC,CAAC;QACpB,MAAMc,UAAU,GAAG,IAAI,CAACA,UAAU;QAClC;QACA;QACA,IAAI,CAAAA,UAAU,aAAVA,UAAU,uBAAVA,UAAU,CAAEQ,MAAM,MAAKF,KAAK,CAACE,MAAM,EAAE;UAAA,IAAAmC,iBAAA;UACvC,IAAI3C,UAAU,EAAEA,UAAU,CAACoB,KAAK,GAAG,KAAK;UACxC,IAAI,EAAAuB,iBAAA,OAAI,CAAC1C,UAAU,cAAA0C,iBAAA,uBAAfA,iBAAA,CAAiBnC,MAAM,MAAKF,KAAK,CAACE,MAAM,EAC1C,IAAI,CAACP,UAAU,GAAGP,SAAS;QAC/B;QACA;QACA;QACA,IACG,IAAI,CAACI,OAAO,CAACoC,GAAG,CAAC5B,KAAK,CAACE,MAAM,CAAC,KAC5B,CAAAR,UAAU,aAAVA,UAAU,uBAAVA,UAAU,CAAEQ,MAAM,MAAKF,KAAK,CAACE,MAAM,IAAI,CAACR,UAAU,CAACoB,KAAK,CAAC,IAC5D,EAAAsB,iBAAA,OAAI,CAACzC,UAAU,cAAAyC,iBAAA,uBAAfA,iBAAA,CAAiBlC,MAAM,MAAKF,KAAK,CAACE,MAAM,EAExC;QACF,IAAI,CAACP,UAAU,GAAG;UAAEO,MAAM,EAAEF,KAAK,CAACE,MAAM;UAAEtB;QAAM,CAAC;MACnD;IACF,CAAC;IACD,OAAO,IAAI,CAACU,cAAc;EAC5B;EAEA,OAAeuB,gBAAgBA,CAAA,EAAS;IACtC,MAAMlB,UAAU,GAAG,IAAI,CAACA,UAAU;IAClC,MAAMD,UAAU,GAAG,IAAI,CAACA,UAAU;IAClC,IACE,CAACC,UAAU,IACXA,UAAU,CAACgB,GAAG,KAAKvB,SAAS,IAC5B,CAACM,UAAU,IACXA,UAAU,CAACQ,MAAM,KAAKP,UAAU,CAACO,MAAM,EAEvC;IACF,IAAI,CAACP,UAAU,GAAGP,SAAS;IAC3B,MAAMkD,QAAQ,GAAG5C,UAAU,CAACoB,KAAK;IACjCpB,UAAU,CAACoB,KAAK,GAAG,KAAK;IACxB,MAAMyB,OAAO,GAAG5C,UAAU,CAACgB,GAAG,GAAGhB,UAAU,CAACf,KAAK;IACjD,IACE,CAAC0D,QAAQ,IACT,CAACE,MAAM,CAACC,QAAQ,CAACF,OAAO,CAAC,IACzBA,OAAO,IAAI,CAAC,IACZA,OAAO,IAAIlE,iBAAiB,EAE5B;IACF,IAAI,CAACM,OAAO,CAAC+D,iBAAiB,CAC5BhD,UAAU,CAACV,KAAK,CAAC2C,GAAG,EACpBjC,UAAU,CAACuC,UAAU,EACrBU,IAAI,CAACC,KAAK,CAACL,OAAO,GAAG,OAAO,CAC9B,CAAC;EACH;EAEA,OAAelC,gBAAgBA,CAACH,MAAc,EAAQ;IAAA,IAAA2C,iBAAA,EAAAC,iBAAA;IACpD,IAAI,EAAAD,iBAAA,OAAI,CAAClD,UAAU,cAAAkD,iBAAA,uBAAfA,iBAAA,CAAiB3C,MAAM,MAAKA,MAAM,EAAE,IAAI,CAACP,UAAU,GAAGP,SAAS;IACnE,IAAI,EAAA0D,iBAAA,OAAI,CAACpD,UAAU,cAAAoD,iBAAA,uBAAfA,iBAAA,CAAiB5C,MAAM,MAAKA,MAAM,EAAE,IAAI,CAACR,UAAU,CAACoB,KAAK,GAAG,KAAK;EACvE;EAEA,OAAeF,YAAYA,CAAA,EAAW;IACpC,OAAO,OAAOmC,WAAW,KAAK,WAAW,IACvC,OAAOA,WAAW,CAACC,GAAG,KAAK,UAAU,GACnCD,WAAW,CAACC,GAAG,CAAC,CAAC,GACjBC,GAAG;EACT;EAEA,OAAe1B,SAASA,CACtBvC,KAA+B,EACL;IAC1B,QAAAkE,MAAA,GAAOlE,KAAK,cAAAkE,MAAA,eAALA,MAAA,CAAOlC,KAAK,EAAE;MAAA,IAAAkC,MAAA,EAAAC,kBAAA;MACnB,MAAMC,KAAK,GAAGpE,KAAK,CAACgC,KAAK,CAACI,MAAM,EAAA+B,kBAAA,GAACnE,KAAK,CAACgC,KAAK,CAACK,KAAK,cAAA8B,kBAAA,cAAAA,kBAAA,GAAI,CAAC,CAAC;MACxD,IAAI,CAACC,KAAK,EAAE;MACZpE,KAAK,GAAGoE,KAAK;IACf;IACA,OAAOpE,KAAK;EACd;EAEA,OAAeE,IAAIA,CAACF,KAAmB,EAAsB;IAC3D,IAAI,CAACY,WAAW,GAAGZ,KAAK;IACxB,OAAO,IAAI,CAACL,OAAO,CAACO,IAAI,CAACF,KAAK,CAAC;EACjC;AACF;AAACqE,6BAAA,GAzMY/E,4BAA4B;AAAAgF,eAAA,CAA5BhF,4BAA4B,aACd,IAAIF,iBAAiB,CAAC,MAAM;EACnDiF,6BAAA,CAAK1D,UAAU,GAAGP,SAAS;EAC3B,IAAIiE,6BAAA,CAAK3D,UAAU,EAAE2D,6BAAA,CAAK3D,UAAU,CAACoB,KAAK,GAAG,KAAK;AACpD,CAAC,CAAC;AAAAwC,eAAA,CAJShF,4BAA4B,yBAKa,IAAI;AAAAgF,eAAA,CAL7ChF,4BAA4B;AAAAgF,eAAA,CAA5BhF,4BAA4B,eAOZ,CAAC;AAAAgF,eAAA,CAPjBhF,4BAA4B;AAAAgF,eAAA,CAA5BhF,4BAA4B,aASd,IAAImD,GAAG,CAAS,CAAC;AAAA6B,eAAA,CAT/BhF,4BAA4B;AAAAgF,eAAA,CAA5BhF,4BAA4B;AAAAgF,eAAA,CAA5BhF,4BAA4B","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ export { FTRumReactNavigationTracking } from './FTRumReactNavigationTracking';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["FTRumReactNavigationTracking"],"sources":["index.ts"],"sourcesContent":["export { FTRumReactNavigationTracking } from './FTRumReactNavigationTracking';\nexport type { FTRumStackListeners } from './FTRumReactNavigationTracking';\n"],"mappings":"AAAA,SAASA,4BAA4B,QAAQ,gCAAgC","ignoreList":[]}
@@ -0,0 +1,50 @@
1
+ import type { NavigationContainerRef, ParamListBase } from '@react-navigation/native';
2
+ type TrackedRoute = {
3
+ key: string;
4
+ name: string;
5
+ state?: {
6
+ index?: number;
7
+ routes: readonly TrackedRoute[];
8
+ };
9
+ };
10
+ type StackEvent = {
11
+ target?: string;
12
+ data?: {
13
+ closing?: boolean;
14
+ state?: {
15
+ index?: number;
16
+ routes: readonly TrackedRoute[];
17
+ };
18
+ };
19
+ };
20
+ export type FTRumStackListeners = {
21
+ focus(event: StackEvent): void;
22
+ state(event: StackEvent): void;
23
+ transitionStart(event: StackEvent): void;
24
+ transitionEnd(event: StackEvent): void;
25
+ gestureCancel(event: StackEvent): void;
26
+ };
27
+ /** One active navigation container, using either route or stack tracking. */
28
+ export declare class FTRumReactNavigationTracking {
29
+ private static tracker;
30
+ private static registeredContainer;
31
+ private static unsubscribe?;
32
+ private static lifecycle;
33
+ private static stackListeners?;
34
+ private static visited;
35
+ private static appearance?;
36
+ private static transition?;
37
+ static trackRouter: TrackedRoute | undefined;
38
+ /** Call from NavigationContainer.onReady. This mode does not measure loading. */
39
+ static startTrackingViews<ParamList extends ParamListBase>(ref: NavigationContainerRef<ParamList> | null): void;
40
+ /** Omit ref to dispose stack tracking. Old containers cannot stop a new one. */
41
+ static stopTrackingViews(ref?: object | null): void;
42
+ /** Track Views on state changes; time first focus through opening transitionEnd. */
43
+ static get StackListener(): FTRumStackListeners;
44
+ private static finishTransition;
45
+ private static cancelTransition;
46
+ private static monotonicNow;
47
+ private static leafRoute;
48
+ private static show;
49
+ }
50
+ export {};
@@ -0,0 +1,2 @@
1
+ export { FTRumReactNavigationTracking } from './FTRumReactNavigationTracking';
2
+ export type { FTRumStackListeners } from './FTRumReactNavigationTracking';
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@truewatchtech/mobile-react-navigation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.4.3",
4
+ "description": "Automatic TrueWatch RUM View tracking for @react-navigation/native.",
5
+ "main": "lib/commonjs/index",
6
+ "module": "lib/module/index",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index",
9
+ "source": "src/index",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "README.md",
14
+ "!**/__tests__",
15
+ "!**/__mocks__"
16
+ ],
17
+ "scripts": {
18
+ "lint": "yarn run -T eslint \"src/**/*.{ts,tsx}\" \"tests/**/*.ts\"",
19
+ "prepare": "yarn run -T bob build",
20
+ "prepack": "yarn run -T check-version && yarn prepare",
21
+ "test": "yarn run -T jest",
22
+ "typescript": "yarn run -T tsc --noEmit"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/TrueWatchTech/datakit-react-native.git"
27
+ },
28
+ "license": "Apache-2.0",
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ },
33
+ "peerDependencies": {
34
+ "@truewatchtech/react-native-mobile": "^0.4.3",
35
+ "react": ">=16.13.1",
36
+ "react-native": ">=0.63.4 <1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@react-navigation/native": "^6.1.18",
40
+ "@react-navigation/native-stack": "^6.11.0",
41
+ "react-native-safe-area-context": "^3.3.2",
42
+ "react-native-screens": "^3.0.0"
43
+ },
44
+ "react-native-builder-bob": {
45
+ "source": "src",
46
+ "output": "lib",
47
+ "targets": [
48
+ "commonjs",
49
+ "module",
50
+ [
51
+ "typescript",
52
+ {
53
+ "tsc": "../../node_modules/.bin/tsc",
54
+ "project": "tsconfig.build.json"
55
+ }
56
+ ]
57
+ ]
58
+ },
59
+ "jest": {
60
+ "preset": "react-native",
61
+ "modulePathIgnorePatterns": [
62
+ "<rootDir>/lib/"
63
+ ]
64
+ },
65
+ "gitHead": "61ed39427761a210cbfe6b339e71296f952f9697"
6
66
  }
@@ -0,0 +1,238 @@
1
+ import type {
2
+ NavigationContainerRef,
3
+ ParamListBase,
4
+ } from '@react-navigation/native';
5
+ import { FTRumViewTracking } from '@truewatchtech/react-native-mobile';
6
+
7
+ type TrackedRoute = {
8
+ key: string;
9
+ name: string;
10
+ state?: { index?: number; routes: readonly TrackedRoute[] };
11
+ };
12
+ type StackEvent = {
13
+ target?: string;
14
+ data?: {
15
+ closing?: boolean;
16
+ state?: { index?: number; routes: readonly TrackedRoute[] };
17
+ };
18
+ };
19
+ type Appearance = {
20
+ target: string;
21
+ route: { key: string; name: string };
22
+ generation?: number;
23
+ first: boolean;
24
+ };
25
+ type Transition = { target: string; start: number; end?: number };
26
+ const MAX_TRANSITION_MS = 15 * 60 * 1000;
27
+
28
+ export type FTRumStackListeners = {
29
+ focus(event: StackEvent): void;
30
+ state(event: StackEvent): void;
31
+ transitionStart(event: StackEvent): void;
32
+ transitionEnd(event: StackEvent): void;
33
+ gestureCancel(event: StackEvent): void;
34
+ };
35
+
36
+ /** One active navigation container, using either route or stack tracking. */
37
+ export class FTRumReactNavigationTracking {
38
+ private static tracker = new FTRumViewTracking(() => {
39
+ this.transition = undefined;
40
+ if (this.appearance) this.appearance.first = false;
41
+ });
42
+ private static registeredContainer: object | null = null;
43
+ private static unsubscribe?: () => void;
44
+ private static lifecycle = 0;
45
+ private static stackListeners?: FTRumStackListeners;
46
+ private static visited = new Set<string>();
47
+ private static appearance?: Appearance;
48
+ private static transition?: Transition;
49
+ static trackRouter: TrackedRoute | undefined;
50
+
51
+ /** Call from NavigationContainer.onReady. This mode does not measure loading. */
52
+ static startTrackingViews<ParamList extends ParamListBase>(
53
+ ref: NavigationContainerRef<ParamList> | null
54
+ ): void {
55
+ if (!ref || this.registeredContainer === ref) return;
56
+ this.stopTrackingViews();
57
+ this.registeredContainer = ref;
58
+ this.tracker.start();
59
+ const lifecycle = this.lifecycle;
60
+ this.unsubscribe = ref.addListener('state', () => {
61
+ if (lifecycle !== this.lifecycle) return;
62
+ const route = ref.getCurrentRoute();
63
+ if (route) this.show(route);
64
+ });
65
+ const route = ref.getCurrentRoute();
66
+ if (route) this.show(route);
67
+ }
68
+
69
+ /** Omit ref to dispose stack tracking. Old containers cannot stop a new one. */
70
+ static stopTrackingViews(ref?: object | null): void {
71
+ if (ref !== undefined && ref !== this.registeredContainer) return;
72
+ ++this.lifecycle;
73
+ this.unsubscribe?.();
74
+ this.unsubscribe = undefined;
75
+ this.registeredContainer = null;
76
+ this.stackListeners = undefined;
77
+ this.tracker.stop();
78
+ this.visited.clear();
79
+ this.appearance = undefined;
80
+ this.transition = undefined;
81
+ this.trackRouter = undefined;
82
+ }
83
+
84
+ /** Track Views on state changes; time first focus through opening transitionEnd. */
85
+ static get StackListener(): FTRumStackListeners {
86
+ if (this.stackListeners) return this.stackListeners;
87
+ this.stopTrackingViews();
88
+ const lifecycle = this.lifecycle;
89
+ const current = (): boolean => lifecycle === this.lifecycle;
90
+ this.stackListeners = {
91
+ transitionStart: (event): void => {
92
+ if (current() && event.target && event.data?.closing === true)
93
+ this.cancelTransition(event.target);
94
+ },
95
+ transitionEnd: (event): void => {
96
+ const end = this.monotonicNow();
97
+ if (!current() || !event.target) return;
98
+ if (event.data?.closing === true) {
99
+ this.cancelTransition(event.target);
100
+ return;
101
+ }
102
+ if (event.data?.closing !== false) {
103
+ this.cancelTransition(event.target);
104
+ return;
105
+ }
106
+ if (this.transition?.target === event.target) {
107
+ // Capture the end now, even when the matching state arrives later.
108
+ if (this.transition.end === undefined) this.transition.end = end;
109
+ this.finishTransition();
110
+ } else if (this.appearance?.target === event.target) {
111
+ // No observed focus: do not infer a start from a later callback.
112
+ this.appearance.first = false;
113
+ }
114
+ },
115
+ gestureCancel: (): void => {
116
+ if (!current()) return;
117
+ this.transition = undefined;
118
+ if (this.appearance) this.appearance.first = false;
119
+ },
120
+ state: (event): void => {
121
+ if (!current()) return;
122
+ const state = event.data?.state;
123
+ if (!state) return;
124
+ const outer = state.routes[state.index ?? 0];
125
+ const leaf = this.leafRoute(outer);
126
+ if (!outer || !leaf) {
127
+ this.tracker.stop();
128
+ this.visited.clear();
129
+ this.appearance = undefined;
130
+ this.transition = undefined;
131
+ this.trackRouter = undefined;
132
+ return;
133
+ }
134
+ // Retain instance history only while its route remains in this stack.
135
+ const keys = new Set(state.routes.map((route) => route.key));
136
+ for (const key of this.visited) {
137
+ if (!keys.has(key)) this.visited.delete(key);
138
+ }
139
+ if (this.transition && this.transition.target !== outer.key)
140
+ this.transition = undefined;
141
+ const previous = this.appearance;
142
+ if (previous?.target !== outer.key || previous.route.key !== leaf.key) {
143
+ const route = { key: leaf.key, name: leaf.name };
144
+ const first = !this.visited.has(outer.key) && outer.key === leaf.key;
145
+ this.visited.add(outer.key);
146
+ this.appearance = {
147
+ target: outer.key,
148
+ route,
149
+ first,
150
+ generation: this.show(route),
151
+ };
152
+ } else {
153
+ this.tracker.setCurrent(previous.route);
154
+ }
155
+ this.finishTransition();
156
+ },
157
+ focus: (event): void => {
158
+ const start = this.monotonicNow();
159
+ if (!current() || !event.target) return;
160
+ this.tracker.start();
161
+ const appearance = this.appearance;
162
+ // A newly selected route invalidates the previous appearance even if
163
+ // React Navigation has not delivered the matching state update yet.
164
+ if (appearance?.target !== event.target) {
165
+ if (appearance) appearance.first = false;
166
+ if (this.transition?.target !== event.target)
167
+ this.transition = undefined;
168
+ }
169
+ // Keep the first focus boundary. Existing instances (including nested
170
+ // containers) may open Views again, but never start another load timer.
171
+ if (
172
+ (this.visited.has(event.target) &&
173
+ (appearance?.target !== event.target || !appearance.first)) ||
174
+ this.transition?.target === event.target
175
+ )
176
+ return;
177
+ this.transition = { target: event.target, start };
178
+ },
179
+ };
180
+ return this.stackListeners;
181
+ }
182
+
183
+ private static finishTransition(): void {
184
+ const transition = this.transition;
185
+ const appearance = this.appearance;
186
+ if (
187
+ !transition ||
188
+ transition.end === undefined ||
189
+ !appearance ||
190
+ appearance.target !== transition.target
191
+ )
192
+ return;
193
+ this.transition = undefined;
194
+ const eligible = appearance.first;
195
+ appearance.first = false;
196
+ const elapsed = transition.end - transition.start;
197
+ if (
198
+ !eligible ||
199
+ !Number.isFinite(elapsed) ||
200
+ elapsed <= 0 ||
201
+ elapsed >= MAX_TRANSITION_MS
202
+ )
203
+ return;
204
+ this.tracker.updateLoadingTime(
205
+ appearance.route.key,
206
+ appearance.generation,
207
+ Math.floor(elapsed * 1000000)
208
+ );
209
+ }
210
+
211
+ private static cancelTransition(target: string): void {
212
+ if (this.transition?.target === target) this.transition = undefined;
213
+ if (this.appearance?.target === target) this.appearance.first = false;
214
+ }
215
+
216
+ private static monotonicNow(): number {
217
+ return typeof performance !== 'undefined' &&
218
+ typeof performance.now === 'function'
219
+ ? performance.now()
220
+ : NaN;
221
+ }
222
+
223
+ private static leafRoute(
224
+ route: TrackedRoute | undefined
225
+ ): TrackedRoute | undefined {
226
+ while (route?.state) {
227
+ const child = route.state.routes[route.state.index ?? 0];
228
+ if (!child) break;
229
+ route = child;
230
+ }
231
+ return route;
232
+ }
233
+
234
+ private static show(route: TrackedRoute): number | undefined {
235
+ this.trackRouter = route;
236
+ return this.tracker.show(route);
237
+ }
238
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { FTRumReactNavigationTracking } from './FTRumReactNavigationTracking';
2
+ export type { FTRumStackListeners } from './FTRumReactNavigationTracking';