@symbiote-native/app-metrics 0.0.0-stage → 0.1.0

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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/build/angular/app-metrics-root.d.ts +5 -0
  4. package/build/angular/app-metrics-root.js +62 -0
  5. package/build/angular/index.d.ts +3 -0
  6. package/build/angular/index.js +5 -0
  7. package/build/angular/inject-network-request-observer.d.ts +5 -0
  8. package/build/angular/inject-network-request-observer.js +7 -0
  9. package/build/core/app-metrics.d.ts +24 -0
  10. package/build/core/app-metrics.js +47 -0
  11. package/build/core/index.d.ts +5 -0
  12. package/build/core/index.js +6 -0
  13. package/build/core/install-error-handler.d.ts +2 -0
  14. package/build/core/install-error-handler.js +45 -0
  15. package/build/core/native-module.d.ts +19 -0
  16. package/build/core/native-module.js +3 -0
  17. package/build/core/network-request-observer-lifecycle.d.ts +8 -0
  18. package/build/core/network-request-observer-lifecycle.js +19 -0
  19. package/build/core/network-request-observer-subscription.d.ts +15 -0
  20. package/build/core/network-request-observer-subscription.js +22 -0
  21. package/build/core/network-request-observer.d.ts +7 -0
  22. package/build/core/network-request-observer.js +1 -0
  23. package/build/core/report-caught-error.d.ts +2 -0
  24. package/build/core/report-caught-error.js +20 -0
  25. package/build/core/session.d.ts +12 -0
  26. package/build/core/session.js +1 -0
  27. package/build/core/types.d.ts +168 -0
  28. package/build/core/types.js +3 -0
  29. package/build/react/app-metrics-error-boundary.d.ts +26 -0
  30. package/build/react/app-metrics-error-boundary.js +28 -0
  31. package/build/react/app-metrics-root.d.ts +12 -0
  32. package/build/react/app-metrics-root.js +24 -0
  33. package/build/react/index.d.ts +4 -0
  34. package/build/react/index.js +4 -0
  35. package/build/react/use-network-request-observer.d.ts +5 -0
  36. package/build/react/use-network-request-observer.js +23 -0
  37. package/build/solid/app-metrics-error-boundary.d.ts +15 -0
  38. package/build/solid/app-metrics-error-boundary.js +17 -0
  39. package/build/solid/app-metrics-root.d.ts +9 -0
  40. package/build/solid/app-metrics-root.js +18 -0
  41. package/build/solid/index.d.ts +4 -0
  42. package/build/solid/index.js +4 -0
  43. package/build/solid/use-network-request-observer.d.ts +5 -0
  44. package/build/solid/use-network-request-observer.js +6 -0
  45. package/build/svelte/AppMetricsErrorBoundary.svelte +17 -0
  46. package/build/svelte/AppMetricsErrorBoundary.svelte.d.ts +4 -0
  47. package/build/svelte/AppMetricsRoot.svelte +22 -0
  48. package/build/svelte/AppMetricsRoot.svelte.d.ts +4 -0
  49. package/build/svelte/app-metrics-error-boundary-props.d.ts +12 -0
  50. package/build/svelte/app-metrics-error-boundary-props.js +3 -0
  51. package/build/svelte/app-metrics-root-props.d.ts +9 -0
  52. package/build/svelte/app-metrics-root-props.js +1 -0
  53. package/build/svelte/index.d.ts +7 -0
  54. package/build/svelte/index.js +4 -0
  55. package/build/svelte/use-network-request-observer.svelte.d.ts +3 -0
  56. package/build/svelte/use-network-request-observer.svelte.js +14 -0
  57. package/build/vue/app-metrics-error-boundary.d.ts +14 -0
  58. package/build/vue/app-metrics-error-boundary.js +27 -0
  59. package/build/vue/app-metrics-root.d.ts +7 -0
  60. package/build/vue/app-metrics-root.js +15 -0
  61. package/build/vue/index.d.ts +4 -0
  62. package/build/vue/index.js +4 -0
  63. package/build/vue/use-network-request-observer.d.ts +5 -0
  64. package/build/vue/use-network-request-observer.js +6 -0
  65. package/build-ngc/angular/app-metrics-root.d.ts +8 -0
  66. package/build-ngc/angular/app-metrics-root.js +20 -0
  67. package/build-ngc/angular/app-metrics-root.js.map +1 -0
  68. package/build-ngc/angular/index.d.ts +3 -0
  69. package/build-ngc/angular/index.js +6 -0
  70. package/build-ngc/angular/index.js.map +1 -0
  71. package/build-ngc/angular/inject-network-request-observer.d.ts +5 -0
  72. package/build-ngc/angular/inject-network-request-observer.js +8 -0
  73. package/build-ngc/angular/inject-network-request-observer.js.map +1 -0
  74. package/build-ngc/core/app-metrics.d.ts +24 -0
  75. package/build-ngc/core/app-metrics.js +48 -0
  76. package/build-ngc/core/app-metrics.js.map +1 -0
  77. package/build-ngc/core/index.d.ts +5 -0
  78. package/build-ngc/core/index.js +7 -0
  79. package/build-ngc/core/index.js.map +1 -0
  80. package/build-ngc/core/install-error-handler.d.ts +2 -0
  81. package/build-ngc/core/install-error-handler.js +46 -0
  82. package/build-ngc/core/install-error-handler.js.map +1 -0
  83. package/build-ngc/core/native-module.d.ts +19 -0
  84. package/build-ngc/core/native-module.js +4 -0
  85. package/build-ngc/core/native-module.js.map +1 -0
  86. package/build-ngc/core/network-request-observer-lifecycle.d.ts +8 -0
  87. package/build-ngc/core/network-request-observer-lifecycle.js +20 -0
  88. package/build-ngc/core/network-request-observer-lifecycle.js.map +1 -0
  89. package/build-ngc/core/network-request-observer-subscription.d.ts +15 -0
  90. package/build-ngc/core/network-request-observer-subscription.js +23 -0
  91. package/build-ngc/core/network-request-observer-subscription.js.map +1 -0
  92. package/build-ngc/core/network-request-observer.d.ts +7 -0
  93. package/build-ngc/core/network-request-observer.js +2 -0
  94. package/build-ngc/core/network-request-observer.js.map +1 -0
  95. package/build-ngc/core/report-caught-error.d.ts +2 -0
  96. package/build-ngc/core/report-caught-error.js +21 -0
  97. package/build-ngc/core/report-caught-error.js.map +1 -0
  98. package/build-ngc/core/session.d.ts +12 -0
  99. package/build-ngc/core/session.js +2 -0
  100. package/build-ngc/core/session.js.map +1 -0
  101. package/build-ngc/core/types.d.ts +168 -0
  102. package/build-ngc/core/types.js +4 -0
  103. package/build-ngc/core/types.js.map +1 -0
  104. package/native-link.json +12 -0
  105. package/package.json +147 -3
  106. package/src/angular/app-metrics-root.ts +14 -0
  107. package/src/angular/index.ts +8 -0
  108. package/src/angular/inject-network-request-observer.ts +18 -0
  109. package/src/core/app-metrics.ts +74 -0
  110. package/src/core/index.ts +52 -0
  111. package/src/core/install-error-handler.ts +55 -0
  112. package/src/core/native-module.ts +36 -0
  113. package/src/core/network-request-observer-lifecycle.ts +37 -0
  114. package/src/core/network-request-observer-subscription.ts +53 -0
  115. package/src/core/network-request-observer.ts +15 -0
  116. package/src/core/report-caught-error.ts +26 -0
  117. package/src/core/session.ts +17 -0
  118. package/src/core/types.ts +200 -0
  119. package/src/react/app-metrics-error-boundary.tsx +67 -0
  120. package/src/react/app-metrics-root.tsx +53 -0
  121. package/src/react/index.ts +11 -0
  122. package/src/react/use-network-request-observer.ts +41 -0
  123. package/src/solid/app-metrics-error-boundary.tsx +36 -0
  124. package/src/solid/app-metrics-root.tsx +29 -0
  125. package/src/solid/index.ts +12 -0
  126. package/src/solid/use-network-request-observer.ts +17 -0
  127. package/src/svelte/AppMetricsErrorBoundary.svelte +17 -0
  128. package/src/svelte/AppMetricsRoot.svelte +21 -0
  129. package/src/svelte/app-metrics-error-boundary-props.ts +17 -0
  130. package/src/svelte/app-metrics-root-props.ts +10 -0
  131. package/src/svelte/index.ts +10 -0
  132. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  133. package/src/svelte/use-network-request-observer.svelte.ts +26 -0
  134. package/src/vue/app-metrics-error-boundary.ts +56 -0
  135. package/src/vue/app-metrics-root.ts +32 -0
  136. package/src/vue/index.ts +12 -0
  137. package/src/vue/use-network-request-observer.ts +17 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,184 @@
1
- # Temporary Holding Version
1
+ # @symbiote-native/app-metrics
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
+ See how your app really performs on users' devices: how long it takes to start and become
4
+ interactive, how it handles network requests, when it crashes. One API for every
5
+ [SymbioteNative](../../README.md) adapter (React, Vue, Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-app-metrics`](https://github.com/expo/expo/tree/main/packages/expo-app-metrics)
8
+ (app startup, frame rate, memory, network-request, crash, and session metrics): the core surface on
9
+ every adapter, plus an `AppMetricsRoot` on all five and an `AppMetricsErrorBoundary` on React, Vue,
10
+ Solid, and Svelte, each wrapping that framework's own catch primitive.
11
+
12
+ ## Scope decision (2026-09-28)
13
+
14
+ Unlike every other package in this catalog, `expo-app-metrics` ships real per-framework
15
+ surface, not just a thin async-function wrapper. React, Vue, Solid, and Svelte each already have
16
+ a subtree-scoped error-catch mechanism (React's class boundary, Vue's `onErrorCaptured`, Solid's
17
+ built-in `ErrorBoundary`, Svelte 5's `<svelte:boundary>`), so `AppMetricsErrorBoundary` wraps
18
+ that mechanism on all four, sharing the report-building logic through
19
+ `core/report-caught-error.ts`. `useNetworkRequestObserver`/`injectNetworkRequestObserver` ship on
20
+ all five adapters (see "API" below).
21
+
22
+ **Angular is NOT a permanent gap - it is version-gated.** Angular's own `@boundary`/`@error`
23
+ template primitive (stable `@publicApi` since `@angular/core` 22.2.0) is a real, DI-resolved
24
+ subtree error boundary (`ErrorHandler.onViewError`), the same caught-vs-uncaught split this
25
+ package already uses elsewhere. This repo pins `@angular/core` to `~22.0.8`
26
+ (`pnpm-workspace.yaml`) because `@angular/compiler-cli` >=22.1.0 bundles its own
27
+ `@babel/core@8` and its AOT linker asserts Babel 8, while this repo loads that linker INSIDE
28
+ Metro's own Babel 7 pipeline - re-tried bumping to 22.2.0 for this package (2026-09-28),
29
+ reproduced the exact `assertVersion(8)` throw directly against the real installed Babel, no
30
+ legacy Babel-7 linker build exists, reverted. `AppMetricsRoot` needed no version bump (it is a
31
+ plain `<ng-content>` wrapper marking first render in `ngOnInit`) and ships on `./angular` now.
32
+ `AppMetricsErrorBoundary` for Angular - as an `AppMetricsBoundaryErrorHandler` the consumer
33
+ provides on their own `@boundary`-owning component, since Angular's own docs mark
34
+ `<ng-content>` inside `@boundary`/`@error` an antipattern, so a reusable wrapper COMPONENT is
35
+ not the right shape there either - ships once the catalog can move past 22.0.8.
36
+
37
+ ## Install
38
+
39
+ **New app:**
40
+
41
+ ```bash
42
+ npx @symbiote-native/cli new my-app --app-metrics
43
+ ```
44
+
45
+ **Existing SymbioteNative app:**
46
+
47
+ ```bash
48
+ npx @symbiote-native/cli add --app-metrics
49
+ ```
50
+
51
+ Either way: installs `@symbiote-native/app-metrics` and wires the native autolinking
52
+ automatically, see [`@symbiote-native/cli`](../cli).
53
+
54
+ <details>
55
+ <summary>Manual install (no CLI, wiring native autolinking by hand)</summary>
56
+
57
+ ```bash
58
+ npm install @symbiote-native/app-metrics
59
+ ```
60
+
61
+ `expo-app-metrics` and `expo-modules-core` come along as regular dependencies, pinned to exact
62
+ versions. Never install either yourself, and never add the `expo` meta-package to your project
63
+ (it bundles its own Metro/Babel pipeline, which conflicts with this project's own).
64
+
65
+ ## Required one-time step: native autolinking wiring
66
+
67
+ Unlike a plain RN native module, `expo-app-metrics`'s native code is discovered by
68
+ `expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism. This needs wiring
69
+ into the native host app **once**, covering this package and every other `expo-modules-core`
70
+ package with zero further changes. Full mechanics live in the `symbiote-expo-native-module`
71
+ project skill.
72
+
73
+ No app-level permission strings are needed for this package.
74
+
75
+ </details>
76
+
77
+ ## Shape
78
+
79
+ ```
80
+ src/core/ markFirstRender / markInteractive / logEvent / setGlobalAttributes /
81
+ clearStoredEntries / getInactiveSessions / getAllCrashReports (android) /
82
+ reportError / getMainSession / getForegroundSession, the Session and
83
+ NetworkRequestObserver classes (re-exported straight off the native module, same
84
+ pattern as @symbiote-native/audio), installErrorHandler (wraps global ErrorUtils,
85
+ installed automatically on import), plus every public type
86
+ src/react/ AppMetricsRoot, AppMetricsErrorBoundary, useNetworkRequestObserver
87
+ src/vue/ AppMetricsRoot, AppMetricsErrorBoundary (over onErrorCaptured), useNetworkRequestObserver
88
+ src/solid/ AppMetricsRoot, AppMetricsErrorBoundary (over solid-js's own ErrorBoundary), useNetworkRequestObserver
89
+ src/svelte/ AppMetricsRoot.svelte, AppMetricsErrorBoundary.svelte (over <svelte:boundary>), useNetworkRequestObserver
90
+ src/angular/ AppMetricsRoot (plain <ng-content> wrapper), injectNetworkRequestObserver, no boundary yet (see Scope decision)
91
+ ```
92
+
93
+ ## API
94
+
95
+ ```ts
96
+ markFirstRender(): void
97
+ markInteractive(attributes?: IMetricAttributes): void
98
+ logEvent(name: string, options?: ILogEventOptions): void
99
+ setGlobalAttributes(attributes?: Record<string, ILogAttributeValue> | null): void
100
+ clearStoredEntries(): Promise<void>
101
+ getInactiveSessions(): Promise<IDebugSession[]> // debug-only
102
+ getAllCrashReports(): Promise<ICrashReport[]> // android only, debug-only
103
+ reportError(error: IReportErrorInput): void
104
+ getMainSession(): Session
105
+ getForegroundSession(): Promise<Session | null>
106
+ installErrorHandler(): void // called automatically on import
107
+
108
+ class Session { id; type; startDate; isActive(); getEndDate(); getMetrics(); getLogs(); addMetric(metric) }
109
+ class NetworkRequestObserver { constructor(filter?); setFilter(filter) } // + addListener/removeListener
110
+ ```
111
+
112
+ `AppMetricsRoot` ships on all five adapters. `AppMetricsErrorBoundary` ships on `./react`,
113
+ `./vue`, `./solid`, and `./svelte` only (see Scope decision for `./angular`):
114
+
115
+ ```tsx
116
+ <AppMetricsRoot errorBoundaryFallback={...}>{children}</AppMetricsRoot>
117
+ <AppMetricsErrorBoundary fallback={...}>{children}</AppMetricsErrorBoundary>
118
+ ```
119
+
120
+ Angular's `AppMetricsRoot` takes no `errorBoundaryFallback` (no boundary to gate yet) - just
121
+ `<app-metrics-root><ng-content /></app-metrics-root>` in a template.
122
+
123
+ `AppMetricsRoot.wrap(App)` is React-only (a static helper on the class).
124
+
125
+ `useNetworkRequestObserver` ships on React, Vue, Solid, and Svelte, taking a getter of
126
+ `{ filter?, onStarted?, onCompleted? }` (a plain object on React) and returning the
127
+ `NetworkRequestObserver`; Angular's twin is `injectNetworkRequestObserver`, same shape.
128
+
129
+ ```ts
130
+ import { markFirstRender, reportError } from '@symbiote-native/app-metrics';
131
+ import { AppMetricsRoot, AppMetricsErrorBoundary } from '@symbiote-native/app-metrics/react';
132
+ // or '/vue', '/solid', '/svelte'; '/angular' exports AppMetricsRoot + injectNetworkRequestObserver only
133
+ ```
134
+
135
+ ## Notes
136
+
137
+ - **`getForegroundSession` is typed `@platform ios` in upstream's own JSDoc, but its native
138
+ module implements it on both platforms**, confirmed by reading `AppMetricsModule.swift` and
139
+ `AppMetricsModule.kt` directly, not the comment. Ported as unconditionally available here.
140
+ - **`getAllCrashReports` is Android-only** on the native module; calling it off Android throws
141
+ `UnavailabilityError`, matching this repo's own convention for every other platform-gated
142
+ function. Upstream itself has no such guard, since it never wraps the raw native module in
143
+ per-platform checks; this package normalizes it the same way every sibling package does.
144
+ - **`installErrorHandler` runs automatically the moment this package is imported**, an eager
145
+ side effect in `src/core/index.ts` matching upstream's own `index.ts`, wrapping React
146
+ Native's global `ErrorUtils` handler so an unhandled JS error reaches `reportError` before
147
+ chaining to whatever handler ran before it.
148
+ - **`AppMetricsErrorBoundary` reports through `core/report-caught-error.ts`, independent of this
149
+ engine's `reportUncaughtError` channel** (`core/engine/src/report-error.ts`). A caught error
150
+ never reaches this engine's own uncaught-error reporting, by design, on any of the four
151
+ adapters that have one - see each adapter's own `render-error-reporting.test.*`.
152
+ - **Vue's `onErrorCaptured` gives no component stack** (its `info` argument is a lifecycle-phase
153
+ string, not a real trace), so the Vue and Solid boundaries call `reportCaughtError(error)` with
154
+ no second argument; only React's class boundary has a real `componentStack` to forward.
155
+ - **Solid's `mount()` and Svelte's `mount()` both rethrow an uncaught error** rather than
156
+ swallowing it (each adapter's own tested contract) - `AppMetricsRoot` without
157
+ `errorBoundaryFallback` on those two adapters leaves that throw to reach the caller, same as
158
+ not wrapping the tree at all.
159
+ - `Session` and `NetworkRequestObserver` are native `SharedObject` classes exposed as class
160
+ properties on the native module (see the `symbiote-expo-native-module` skill's SharedObject
161
+ section) - there is no JS logic to port beyond the re-export, same as
162
+ `@symbiote-native/audio`'s `AudioPlaylist`.
163
+ - The canary `AppMetricsScreen` in the `examples/expo-*` apps exercises the root, the boundary and
164
+ the network observer.
165
+
166
+ ## Common questions
167
+
168
+ - **What does it collect?** Startup (cold/warm launch, bundle load, time to first render), frame
169
+ rate, memory, sessions, and your own events.
170
+ - **Where do the numbers go?** EAS Observe, or any OpenTelemetry-compatible backend.
171
+ - **Time to interactive missing.** You must mark the app interactive yourself.
172
+
173
+ Sources: [Expo docs: Introduction to EAS Observe](https://docs.expo.dev/eas/observe/introduction/),
174
+ [Introducing Observe](https://expo.dev/blog/introducing-observe).
175
+
176
+ ## Test it
177
+
178
+ Core is tested the same way as every other package in this catalog: a fake native-module object
179
+ in place of `requireNativeModule` (`src/core/app-metrics.test.ts`, `src/core/
180
+ install-error-handler.test.ts`). The React layer is a real mount through this repo's own
181
+ Fabric-recording harness (`mount`/`unmount` + `installRecordingFabric`, ADR 0025):
182
+ `src/react/app-metrics-root.test.tsx`, `src/react/app-metrics-error-boundary.test.tsx`, and
183
+ `src/react/use-network-request-observer.test.tsx`, adapted from upstream's own
184
+ `@testing-library/react-native` test suites onto this project's harness.
@@ -0,0 +1,5 @@
1
+ import { type OnInit } from '@angular/core';
2
+ /** Marks the first render, so time-to-first-render is measured without a manual call */
3
+ export declare class AppMetricsRoot implements OnInit {
4
+ ngOnInit(): void;
5
+ }
@@ -0,0 +1,62 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { Component } from '@angular/core';
36
+ import { markFirstRender } from '../core/index.js';
37
+ /** Marks the first render, so time-to-first-render is measured without a manual call */
38
+ let AppMetricsRoot = (() => {
39
+ let _classDecorators = [Component({
40
+ selector: 'app-metrics-root',
41
+ standalone: true,
42
+ template: '<ng-content />',
43
+ })];
44
+ let _classDescriptor;
45
+ let _classExtraInitializers = [];
46
+ let _classThis;
47
+ var AppMetricsRoot = class {
48
+ static { _classThis = this; }
49
+ static {
50
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
51
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
52
+ AppMetricsRoot = _classThis = _classDescriptor.value;
53
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
54
+ __runInitializers(_classThis, _classExtraInitializers);
55
+ }
56
+ ngOnInit() {
57
+ markFirstRender();
58
+ }
59
+ };
60
+ return AppMetricsRoot = _classThis;
61
+ })();
62
+ export { AppMetricsRoot };
@@ -0,0 +1,3 @@
1
+ export * from '../core';
2
+ export { AppMetricsRoot } from './app-metrics-root';
3
+ export { injectNetworkRequestObserver, type IUseNetworkRequestObserverOptions, } from './inject-network-request-observer';
@@ -0,0 +1,5 @@
1
+ // No `AppMetricsErrorBoundary` here - blocked on `@angular/core` >=22.2
2
+ // (this package's README "Scope decision")
3
+ export * from '../core/index.js';
4
+ export { AppMetricsRoot } from './app-metrics-root.js';
5
+ export { injectNetworkRequestObserver, } from './inject-network-request-observer.js';
@@ -0,0 +1,5 @@
1
+ import type { NetworkRequestObserver } from '../core';
2
+ import type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
3
+ export type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
4
+ /** Angular twin of `expo-app-metrics`'s `useNetworkRequestObserver` */
5
+ export declare function injectNetworkRequestObserver(getOptions?: () => IUseNetworkRequestObserverOptions): NetworkRequestObserver;
@@ -0,0 +1,7 @@
1
+ import { DestroyRef, effect, inject } from '@angular/core';
2
+ import { createNetworkRequestObserverLifecycle } from '../core/network-request-observer-lifecycle.js';
3
+ /** Angular twin of `expo-app-metrics`'s `useNetworkRequestObserver` */
4
+ export function injectNetworkRequestObserver(getOptions = () => ({})) {
5
+ const destroyRef = inject(DestroyRef);
6
+ return createNetworkRequestObserverLifecycle(getOptions, filterEffect => effect(filterEffect), cleanup => destroyRef.onDestroy(cleanup));
7
+ }
@@ -0,0 +1,24 @@
1
+ import type { ICrashReport, IDebugSession, ILogAttributeValue, ILogEventOptions, IMetricAttributes, IReportErrorInput } from './types';
2
+ /** Marks the moment the app is ready to measure time to first render */
3
+ export declare function markFirstRender(): void;
4
+ export declare function markInteractive(attributes?: IMetricAttributes): void;
5
+ /** Persisted locally, dispatched on the next flush as an OpenTelemetry log record */
6
+ export declare function logEvent(name: string, options?: ILogEventOptions): void;
7
+ /** Merged into every subsequent metric and log event, per-record keys win on collision */
8
+ export declare function setGlobalAttributes(attributes?: Record<string, ILogAttributeValue> | null): void;
9
+ export declare function clearStoredEntries(): Promise<void>;
10
+ /** Debug-only, ordered most-recent-first, each session eagerly includes metrics/logs/crash */
11
+ export declare function getInactiveSessions(): Promise<IDebugSession[]>;
12
+ /** Debug-only, includes orphan crashes with `sessionId: null`. @platform android */
13
+ export declare function getAllCrashReports(): Promise<ICrashReport[]>;
14
+ /** Called by `installErrorHandler`'s global handler and `AppMetricsErrorBoundary` */
15
+ export declare function reportError(error: IReportErrorInput): void;
16
+ /** The per-launch session, a static reference: repeated calls return the same object */
17
+ export declare function getMainSession(): import("./session").Session;
18
+ /** Created when the app becomes active, ended when backgrounded, `null` when none is active */
19
+ export declare function getForegroundSession(): Promise<import("./session").Session | null>;
20
+ export declare const NetworkRequestObserver: typeof import("./network-request-observer").NetworkRequestObserver;
21
+ export type NetworkRequestObserver = InstanceType<typeof NetworkRequestObserver>;
22
+ /** Not intended to be constructed from user code, see `getMainSession`/`getForegroundSession` */
23
+ export declare const Session: typeof import("./session").Session;
24
+ export type Session = InstanceType<typeof Session>;
@@ -0,0 +1,47 @@
1
+ import { Platform, UnavailabilityError } from 'expo-modules-core';
2
+ import { expoAppMetrics } from './native-module.js';
3
+ const NATIVE_MODULE_NAME = 'expo-app-metrics';
4
+ /** Marks the moment the app is ready to measure time to first render */
5
+ export function markFirstRender() {
6
+ expoAppMetrics.markFirstRender();
7
+ }
8
+ export function markInteractive(attributes) {
9
+ expoAppMetrics.markInteractive(attributes);
10
+ }
11
+ /** Persisted locally, dispatched on the next flush as an OpenTelemetry log record */
12
+ export function logEvent(name, options) {
13
+ expoAppMetrics.logEvent(name, options);
14
+ }
15
+ /** Merged into every subsequent metric and log event, per-record keys win on collision */
16
+ export function setGlobalAttributes(attributes) {
17
+ expoAppMetrics.setGlobalAttributes(attributes);
18
+ }
19
+ export async function clearStoredEntries() {
20
+ return expoAppMetrics.clearStoredEntries();
21
+ }
22
+ /** Debug-only, ordered most-recent-first, each session eagerly includes metrics/logs/crash */
23
+ export async function getInactiveSessions() {
24
+ return expoAppMetrics.getInactiveSessions();
25
+ }
26
+ /** Debug-only, includes orphan crashes with `sessionId: null`. @platform android */
27
+ export async function getAllCrashReports() {
28
+ if (Platform.OS !== 'android' || !expoAppMetrics.getAllCrashReports) {
29
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getAllCrashReports');
30
+ }
31
+ return expoAppMetrics.getAllCrashReports();
32
+ }
33
+ /** Called by `installErrorHandler`'s global handler and `AppMetricsErrorBoundary` */
34
+ export function reportError(error) {
35
+ expoAppMetrics.reportError(error);
36
+ }
37
+ /** The per-launch session, a static reference: repeated calls return the same object */
38
+ export function getMainSession() {
39
+ return expoAppMetrics.getMainSession();
40
+ }
41
+ /** Created when the app becomes active, ended when backgrounded, `null` when none is active */
42
+ export async function getForegroundSession() {
43
+ return expoAppMetrics.getForegroundSession();
44
+ }
45
+ export const NetworkRequestObserver = expoAppMetrics.NetworkRequestObserver;
46
+ /** Not intended to be constructed from user code, see `getMainSession`/`getForegroundSession` */
47
+ export const Session = expoAppMetrics.Session;
@@ -0,0 +1,5 @@
1
+ export { clearStoredEntries, getAllCrashReports, getForegroundSession, getInactiveSessions, getMainSession, logEvent, markFirstRender, markInteractive, NetworkRequestObserver, reportError, Session, setGlobalAttributes, } from './app-metrics';
2
+ export { filterKeyOf, subscribeNetworkRequestObserverEvents, } from './network-request-observer-subscription';
3
+ export type { INetworkRequestObserverCallbacks, ISubscribableNetworkRequestObserver, } from './network-request-observer-subscription';
4
+ export type { IAppStartupTimes, ICallStack, ICallStackFrame, ICallStackTree, ICrashReport, IDebugSession, IFrameRateMetrics, ILogAttributeValue, ILogEventOptions, ILogRecord, ILogSeverity, IMemoryUsageSnapshot, IMetric, IMetricAttributes, IMetricInput, INetworkRequestCompletedEvent, INetworkRequestFilter, INetworkRequestObserverEvents, INetworkRequestRedirect, INetworkRequestStartedEvent, IReportErrorInput, ISessionType, } from './types';
5
+ export { installErrorHandler } from './install-error-handler';
@@ -0,0 +1,6 @@
1
+ import { installErrorHandler } from './install-error-handler.js';
2
+ export { clearStoredEntries, getAllCrashReports, getForegroundSession, getInactiveSessions, getMainSession, logEvent, markFirstRender, markInteractive, NetworkRequestObserver, reportError, Session, setGlobalAttributes, } from './app-metrics.js';
3
+ export { filterKeyOf, subscribeNetworkRequestObserverEvents, } from './network-request-observer-subscription.js';
4
+ export { installErrorHandler } from './install-error-handler.js';
5
+ // Live as early as the app pulls this module in, matching upstream's own side effect
6
+ installErrorHandler();
@@ -0,0 +1,2 @@
1
+ /** Wraps React Native's global `ErrorUtils` handler, chaining to whatever ran before it */
2
+ export declare function installErrorHandler(): void;
@@ -0,0 +1,45 @@
1
+ import { reportError } from './app-metrics.js';
2
+ let installed = false;
3
+ // react-native ships its own ambient `ErrorUtils` global, so a package whose tsconfig pulls
4
+ // that in conflicts with a local `declare global` here (core/engine/src/report-error.ts hit
5
+ // this, TS2451) - reading it off `globalThis` through Reflect sidesteps the whole ambient type
6
+ function globalErrorUtils() {
7
+ const utils = Reflect.get(globalThis, 'ErrorUtils');
8
+ if (typeof utils !== 'object' || utils === null)
9
+ return null;
10
+ const getGlobalHandler = Reflect.get(utils, 'getGlobalHandler');
11
+ const setGlobalHandler = Reflect.get(utils, 'setGlobalHandler');
12
+ if (typeof getGlobalHandler !== 'function' ||
13
+ typeof setGlobalHandler !== 'function') {
14
+ return null;
15
+ }
16
+ return {
17
+ getGlobalHandler: () => Reflect.apply(getGlobalHandler, utils, []),
18
+ setGlobalHandler: handler => Reflect.apply(setGlobalHandler, utils, [handler]),
19
+ };
20
+ }
21
+ /** Wraps React Native's global `ErrorUtils` handler, chaining to whatever ran before it */
22
+ export function installErrorHandler() {
23
+ if (installed)
24
+ return;
25
+ const errorUtils = globalErrorUtils();
26
+ if (!errorUtils)
27
+ return;
28
+ installed = true;
29
+ const previousHandler = errorUtils.getGlobalHandler();
30
+ errorUtils.setGlobalHandler((error, isFatal) => {
31
+ try {
32
+ const asError = error instanceof Error ? error : undefined;
33
+ reportError({
34
+ source: 'global',
35
+ type: asError?.name,
36
+ message: asError?.message ?? String(error),
37
+ stacktrace: asError?.stack,
38
+ isFatal: isFatal ?? false,
39
+ });
40
+ }
41
+ finally {
42
+ previousHandler?.(error, isFatal);
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,19 @@
1
+ import type { NetworkRequestObserver } from './network-request-observer';
2
+ import type { Session } from './session';
3
+ import type { ICrashReport, IDebugSession, ILogAttributeValue, ILogEventOptions, IMetricAttributes, IReportErrorInput } from './types';
4
+ export type INativeAppMetricsModule = {
5
+ markFirstRender(): void;
6
+ markInteractive(attributes?: IMetricAttributes): void;
7
+ logEvent(name: string, options?: ILogEventOptions): void;
8
+ setGlobalAttributes(attributes?: Record<string, ILogAttributeValue> | null): void;
9
+ clearStoredEntries(): Promise<void>;
10
+ getInactiveSessions(): Promise<IDebugSession[]>;
11
+ /** Android's native module only, app-metrics.ts gates it on Platform.OS */
12
+ getAllCrashReports?(): Promise<ICrashReport[]>;
13
+ reportError(error: IReportErrorInput): void;
14
+ getMainSession(): Session;
15
+ getForegroundSession(): Promise<Session | null>;
16
+ NetworkRequestObserver: typeof NetworkRequestObserver;
17
+ Session: typeof Session;
18
+ };
19
+ export declare const expoAppMetrics: INativeAppMetricsModule;
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_APP_METRICS_MODULE_NAME = 'ExpoAppMetrics';
3
+ export const expoAppMetrics = requireNativeModule(EXPO_APP_METRICS_MODULE_NAME);
@@ -0,0 +1,8 @@
1
+ import { NetworkRequestObserver } from './app-metrics';
2
+ import type { INetworkRequestObserverCallbacks } from './network-request-observer-subscription';
3
+ import type { INetworkRequestFilter } from './types';
4
+ export type IUseNetworkRequestObserverOptions = INetworkRequestObserverCallbacks & {
5
+ /** Applied natively. Omit or pass `null` to observe every request */
6
+ filter?: INetworkRequestFilter | null;
7
+ };
8
+ export declare function createNetworkRequestObserverLifecycle(getOptions: () => IUseNetworkRequestObserverOptions, runFilterEffect: (effect: () => void) => void, registerCleanup: (cleanup: () => void) => void): NetworkRequestObserver;
@@ -0,0 +1,19 @@
1
+ import { NetworkRequestObserver } from './app-metrics.js';
2
+ import { filterKeyOf, subscribeNetworkRequestObserverEvents, } from './network-request-observer-subscription.js';
3
+ // Shared by adapters whose reactivity auto-tracks reads inside a plain effect (Vue's
4
+ // `watchEffect`, Solid's `createEffect`); React's render model doesn't, so it keeps its own wiring
5
+ export function createNetworkRequestObserverLifecycle(getOptions, runFilterEffect, registerCleanup) {
6
+ const initialFilter = getOptions().filter;
7
+ const observer = new NetworkRequestObserver(initialFilter);
8
+ let appliedFilterKey = filterKeyOf(initialFilter);
9
+ runFilterEffect(() => {
10
+ const nextKey = filterKeyOf(getOptions().filter);
11
+ if (appliedFilterKey === nextKey)
12
+ return;
13
+ appliedFilterKey = nextKey;
14
+ observer.setFilter(getOptions().filter ?? null);
15
+ });
16
+ registerCleanup(subscribeNetworkRequestObserverEvents(observer, getOptions));
17
+ registerCleanup(() => observer.release());
18
+ return observer;
19
+ }
@@ -0,0 +1,15 @@
1
+ import type { INetworkRequestCompletedEvent, INetworkRequestFilter, INetworkRequestStartedEvent } from './types';
2
+ export declare function filterKeyOf(filter: INetworkRequestFilter | null | undefined): string;
3
+ export type INetworkRequestObserverCallbacks = {
4
+ onStarted?: (event: INetworkRequestStartedEvent) => void;
5
+ onCompleted?: (event: INetworkRequestCompletedEvent) => void;
6
+ };
7
+ export type ISubscribableNetworkRequestObserver = {
8
+ addListener(event: 'requestStarted', listener: (event: INetworkRequestStartedEvent) => void): {
9
+ remove: () => void;
10
+ };
11
+ addListener(event: 'requestCompleted', listener: (event: INetworkRequestCompletedEvent) => void): {
12
+ remove: () => void;
13
+ };
14
+ };
15
+ export declare function subscribeNetworkRequestObserverEvents(observer: ISubscribableNetworkRequestObserver, getCallbacks: () => INetworkRequestObserverCallbacks): () => void;
@@ -0,0 +1,22 @@
1
+ // Field-listed rather than a bare JSON.stringify(filter) so `{ hosts, methods }` and
2
+ // `{ methods, hosts }` (the same filter) don't produce different keys
3
+ export function filterKeyOf(filter) {
4
+ return JSON.stringify({
5
+ hosts: filter?.hosts ?? null,
6
+ methods: filter?.methods ?? null,
7
+ });
8
+ }
9
+ // `getCallbacks` is read fresh on every event, so a caller never needs to resubscribe just
10
+ // because its own callbacks changed identity between renders
11
+ export function subscribeNetworkRequestObserverEvents(observer, getCallbacks) {
12
+ const startedSubscription = observer.addListener('requestStarted', event => {
13
+ getCallbacks().onStarted?.(event);
14
+ });
15
+ const completedSubscription = observer.addListener('requestCompleted', event => {
16
+ getCallbacks().onCompleted?.(event);
17
+ });
18
+ return () => {
19
+ startedSubscription.remove();
20
+ completedSubscription.remove();
21
+ };
22
+ }
@@ -0,0 +1,7 @@
1
+ import type { SharedObject } from 'expo-modules-core';
2
+ import type { INetworkRequestFilter, INetworkRequestObserverEvents } from './types';
3
+ export declare class NetworkRequestObserver extends SharedObject<INetworkRequestObserverEvents> {
4
+ constructor(filter?: INetworkRequestFilter | null);
5
+ /** Applies atomically, in-flight events are emitted under either the old or new filter */
6
+ setFilter(filter: INetworkRequestFilter | null): void;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Shared by every adapter's error-boundary equivalent, builds and reports a caught render error */
2
+ export declare function reportCaughtError(error: unknown, componentStack?: string): void;
@@ -0,0 +1,20 @@
1
+ import { reportError } from './app-metrics.js';
2
+ /** Shared by every adapter's error-boundary equivalent, builds and reports a caught render error */
3
+ export function reportCaughtError(error, componentStack) {
4
+ const caught = error instanceof Error ? error : undefined;
5
+ try {
6
+ reportError({
7
+ source: 'errorBoundary',
8
+ type: caught?.name,
9
+ message: caught?.message ?? String(error),
10
+ stacktrace: caught?.stack,
11
+ componentStack,
12
+ isFatal: false,
13
+ });
14
+ }
15
+ catch (reportingError) {
16
+ if (__DEV__) {
17
+ console.warn('[app-metrics] Failed to report a caught error:', reportingError);
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import type { SharedObject } from 'expo-modules-core';
2
+ import type { ILogRecord, IMetric, IMetricInput, ISessionType } from './types';
3
+ export declare class Session extends SharedObject {
4
+ readonly id: string;
5
+ readonly type: ISessionType;
6
+ readonly startDate: string;
7
+ isActive(): Promise<boolean>;
8
+ getEndDate(): Promise<string | null>;
9
+ getMetrics(): Promise<IMetric[]>;
10
+ getLogs(): Promise<ILogRecord[]>;
11
+ addMetric(metric: IMetricInput): Promise<void>;
12
+ }
@@ -0,0 +1 @@
1
+ export {};