@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
@@ -0,0 +1,200 @@
1
+ // Ported verbatim from expo-app-metrics's types.ts (sdk-57), renamed with this repo's
2
+ // `I`-prefix convention
3
+
4
+ export type IAppStartupTimes = {
5
+ /** @platform ios */
6
+ loadTime?: number;
7
+ coldLaunchTime?: number;
8
+ warmLaunchTime?: number;
9
+ bundleLoadTime?: number;
10
+ timeToFirstRender?: number;
11
+ timeToInteractive?: number;
12
+ };
13
+
14
+ export type IMemoryUsageSnapshot = {
15
+ /** @platform ios */
16
+ allocated?: number;
17
+ physical: number;
18
+ available: number;
19
+ /** @platform android */
20
+ javaHeap?: number;
21
+ };
22
+
23
+ export type IFrameRateMetrics = {
24
+ renderedFrames: number;
25
+ expectedFrames: number;
26
+ droppedFrames: number;
27
+ /** Takes at least 700ms to render */
28
+ frozenFrames: number;
29
+ /** Takes at least 17ms to render */
30
+ slowFrames: number;
31
+ freezeTime: number;
32
+ sessionDuration: number;
33
+ };
34
+
35
+ export type IMetric = {
36
+ timestamp: string;
37
+ category: string;
38
+ name: string;
39
+ value: number;
40
+ sessionId: string;
41
+ routeName?: string | null;
42
+ params?: Record<string, unknown>;
43
+ };
44
+
45
+ export type IMetricAttributes = {
46
+ routeName?: string | null;
47
+ params?: Record<string, unknown>;
48
+ };
49
+
50
+ /** Ordered from least to most severe */
51
+ export type ILogSeverity =
52
+ 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
53
+
54
+ export type ILogAttributeValue =
55
+ | string
56
+ | number
57
+ | boolean
58
+ | ILogAttributeValue[]
59
+ | { [key: string]: ILogAttributeValue };
60
+
61
+ export type ILogRecord = {
62
+ timestamp: string;
63
+ name: string;
64
+ body?: string | null;
65
+ attributes?: Record<string, ILogAttributeValue> | null;
66
+ severity: ILogSeverity;
67
+ };
68
+
69
+ export type ILogEventOptions = {
70
+ displayName?: string | null;
71
+ body?: string | null;
72
+ attributes?: Record<string, ILogAttributeValue> | null;
73
+ /** @default 'info' */
74
+ severity?: ILogSeverity | null;
75
+ };
76
+
77
+ export type ISessionType =
78
+ 'main' | 'foreground' | 'screen' | 'custom' | 'unknown';
79
+
80
+ /** Every `IMetric` field except `sessionId`, implied by the owning session */
81
+ export type IMetricInput = Omit<IMetric, 'sessionId'>;
82
+
83
+ export type ICallStackFrame = {
84
+ /** @platform ios */
85
+ binaryName?: string | null;
86
+ /** @platform ios */
87
+ binaryUUID?: string | null;
88
+ /** @platform ios */
89
+ address?: number | null;
90
+ /** @platform ios */
91
+ offsetIntoBinaryTextSegment?: number | null;
92
+ /** @platform ios */
93
+ sampleCount?: number | null;
94
+ subFrames?: ICallStackFrame[] | null;
95
+ /** ios: on-device `dladdr` symbolication. android: the JVM stack-trace frame string */
96
+ symbol?: string | null;
97
+ };
98
+
99
+ export type ICallStack = {
100
+ threadAttributed?: boolean | null;
101
+ callStackRootFrames?: ICallStackFrame[] | null;
102
+ };
103
+
104
+ export type ICallStackTree = {
105
+ callStacks?: ICallStack[] | null;
106
+ };
107
+
108
+ /** ios (MetricKit) reports the numeric fields; android reports `exceptionReason` as a string */
109
+ export type ICrashReport = {
110
+ /** @platform ios */
111
+ exceptionType?: number | null;
112
+ /** @platform ios */
113
+ exceptionCode?: number | null;
114
+ /** Unix signal number, e.g. SIGSEGV = 11 */
115
+ signal?: number | null;
116
+ terminationReason?: string | null;
117
+ /** @platform ios */
118
+ virtualMemoryRegionInfo?: string | null;
119
+ /** ios: a structured MetricKit reason object. android: a plain composed string */
120
+ exceptionReason?:
121
+ | {
122
+ composedMessage: string;
123
+ formatString: string;
124
+ arguments: string[];
125
+ exceptionType: string;
126
+ className: string;
127
+ exceptionName: string;
128
+ }
129
+ | string
130
+ | null;
131
+ callStackTree?: ICallStackTree | null;
132
+ /** `null` for an orphan crash with no attributable session. @platform android */
133
+ sessionId?: string | null;
134
+ appVersion: string;
135
+ timestampBegin: string;
136
+ /** @platform ios */
137
+ timestampEnd?: string;
138
+ ingestedAt: string;
139
+ };
140
+
141
+ export type INetworkRequestStartedEvent = {
142
+ id: string;
143
+ url: string;
144
+ method: string;
145
+ startedAt: string;
146
+ };
147
+
148
+ export type INetworkRequestRedirect = {
149
+ fromUrl: string;
150
+ toUrl: string;
151
+ statusCode: number;
152
+ };
153
+
154
+ export type INetworkRequestCompletedEvent = {
155
+ id: string;
156
+ url: string;
157
+ method: string;
158
+ statusCode: number | null;
159
+ networkProtocol: string | null;
160
+ requestBytesSent: number | null;
161
+ responseBytesReceived: number | null;
162
+ errorDescription: string | null;
163
+ startedAt: string | null;
164
+ completedAt: string | null;
165
+ totalDuration: number;
166
+ redirects: INetworkRequestRedirect[];
167
+ };
168
+
169
+ /** An unset field places no constraint; fields AND together, entries within one OR together */
170
+ export type INetworkRequestFilter = {
171
+ hosts?: string[] | null;
172
+ methods?: string[] | null;
173
+ };
174
+
175
+ export type INetworkRequestObserverEvents = {
176
+ requestStarted(event: INetworkRequestStartedEvent): void;
177
+ requestCompleted(event: INetworkRequestCompletedEvent): void;
178
+ };
179
+
180
+ /** Debug-only, from `getInactiveSessions()` as a plain eager record, not a shared object */
181
+ export type IDebugSession = {
182
+ id: string;
183
+ type: ISessionType;
184
+ startDate: string;
185
+ endDate?: string | null;
186
+ metrics: IMetric[];
187
+ logs: ILogRecord[];
188
+ crashReport?: ICrashReport | null;
189
+ };
190
+
191
+ /** Recorded natively as an OpenTelemetry-shaped `js.exception` log event */
192
+ export type IReportErrorInput = {
193
+ source: 'global' | 'errorBoundary' | 'reportedByUser';
194
+ type?: string;
195
+ message: string;
196
+ stacktrace?: string;
197
+ /** The React component stack, error-boundary captures only */
198
+ componentStack?: string;
199
+ isFatal: boolean;
200
+ };
@@ -0,0 +1,67 @@
1
+ import {
2
+ Component,
3
+ type ErrorInfo,
4
+ type ReactElement,
5
+ type ReactNode,
6
+ } from 'react';
7
+ import { reportCaughtError } from '../core/report-caught-error';
8
+
9
+ export type IAppMetricsErrorBoundaryFallbackProps = {
10
+ /** The value the subtree threw, usually an `Error` but any value can be thrown */
11
+ error: unknown;
12
+ /** Clears the caught error and re-renders the children from a clean, re-mounted state */
13
+ resetError: () => void;
14
+ };
15
+
16
+ export type IAppMetricsErrorBoundaryProps = {
17
+ children: ReactNode;
18
+ /** A React element, a render function receiving `error`/`resetError`, or `null` */
19
+ fallback:
20
+ | ReactElement
21
+ | null
22
+ | ((props: IAppMetricsErrorBoundaryFallbackProps) => ReactNode);
23
+ };
24
+
25
+ type IState = {
26
+ /** Tracked separately from `error` since the thrown value itself can be falsy */
27
+ hasError: boolean;
28
+ error: unknown;
29
+ };
30
+
31
+ /** Records a render-phase error via `reportError` and renders `fallback` in place of it */
32
+ export class AppMetricsErrorBoundary extends Component<
33
+ IAppMetricsErrorBoundaryProps,
34
+ IState
35
+ > {
36
+ override state: IState = { hasError: false, error: null };
37
+
38
+ static getDerivedStateFromError(error: unknown): IState {
39
+ return { hasError: true, error };
40
+ }
41
+
42
+ override componentDidCatch(error: unknown, errorInfo: ErrorInfo): void {
43
+ if (__DEV__) {
44
+ console.warn(
45
+ '[app-metrics] AppMetricsErrorBoundary caught a render error:',
46
+ error,
47
+ );
48
+ }
49
+ reportCaughtError(error, errorInfo.componentStack?.trim() || undefined);
50
+ }
51
+
52
+ override render(): ReactNode {
53
+ if (!this.state.hasError) {
54
+ return this.props.children;
55
+ }
56
+
57
+ const { fallback } = this.props;
58
+ if (typeof fallback === 'function') {
59
+ return fallback({ error: this.state.error, resetError: this.resetError });
60
+ }
61
+ return fallback;
62
+ }
63
+
64
+ private resetError = (): void => {
65
+ this.setState({ hasError: false, error: null });
66
+ };
67
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ useEffect,
3
+ type ComponentType,
4
+ type ReactElement,
5
+ type ReactNode,
6
+ } from 'react';
7
+ import { markFirstRender } from '../core';
8
+ import {
9
+ AppMetricsErrorBoundary,
10
+ type IAppMetricsErrorBoundaryProps,
11
+ } from './app-metrics-error-boundary';
12
+
13
+ export type IAppMetricsRootProps = {
14
+ children: ReactNode;
15
+ /** Omitting it leaves the tree unwrapped; pass `null` to capture without rendering anything */
16
+ errorBoundaryFallback?: IAppMetricsErrorBoundaryProps['fallback'];
17
+ };
18
+
19
+ /** Marks the first render, so time-to-first-render is measured without a manual call */
20
+ export function AppMetricsRoot({
21
+ children,
22
+ errorBoundaryFallback,
23
+ }: IAppMetricsRootProps): ReactElement {
24
+ useEffect(() => {
25
+ markFirstRender();
26
+ }, []);
27
+
28
+ // Only OMITTING the prop leaves the tree unwrapped - an explicit `null` still mounts the
29
+ // boundary and renders nothing, so the error is captured rather than propagating
30
+ if (errorBoundaryFallback !== undefined) {
31
+ return (
32
+ <AppMetricsErrorBoundary fallback={errorBoundaryFallback}>
33
+ {children}
34
+ </AppMetricsErrorBoundary>
35
+ );
36
+ }
37
+ return <>{children}</>;
38
+ }
39
+
40
+ /** Wraps `WrappedComponent` in a root, for one call at the app's entry point */
41
+ AppMetricsRoot.wrap = function wrap<P extends Record<string, unknown>>(
42
+ WrappedComponent: ComponentType<P>,
43
+ ): ComponentType<P> {
44
+ function Wrapped(props: P): ReactElement {
45
+ return (
46
+ <AppMetricsRoot>
47
+ <WrappedComponent {...props} />
48
+ </AppMetricsRoot>
49
+ );
50
+ }
51
+ Wrapped.displayName = `AppMetricsRoot(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
52
+ return Wrapped;
53
+ };
@@ -0,0 +1,11 @@
1
+ export * from '../core';
2
+ export {
3
+ AppMetricsErrorBoundary,
4
+ type IAppMetricsErrorBoundaryFallbackProps,
5
+ type IAppMetricsErrorBoundaryProps,
6
+ } from './app-metrics-error-boundary';
7
+ export { AppMetricsRoot, type IAppMetricsRootProps } from './app-metrics-root';
8
+ export {
9
+ useNetworkRequestObserver,
10
+ type IUseNetworkRequestObserverOptions,
11
+ } from './use-network-request-observer';
@@ -0,0 +1,41 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useReleasingSharedObject } from 'expo-modules-core';
3
+ import { NetworkRequestObserver } from '../core';
4
+ import {
5
+ filterKeyOf,
6
+ subscribeNetworkRequestObserverEvents,
7
+ } from '../core/network-request-observer-subscription';
8
+ import type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
9
+
10
+ export type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
11
+
12
+ /** Allocates a `NetworkRequestObserver` for the component's lifetime, releases it on unmount */
13
+ export function useNetworkRequestObserver(
14
+ options: IUseNetworkRequestObserverOptions = {},
15
+ ): NetworkRequestObserver {
16
+ const filterKey = filterKeyOf(options.filter);
17
+ const observer = useReleasingSharedObject(
18
+ () => new NetworkRequestObserver(options.filter),
19
+ [],
20
+ );
21
+
22
+ // The constructor already applied the initial filter - only a LATER change goes through
23
+ // `setFilter`, seeded with the mount key so the first effect run is a no-op
24
+ const appliedFilterKey = useRef(filterKey);
25
+ useEffect(() => {
26
+ if (appliedFilterKey.current === filterKey) return;
27
+ appliedFilterKey.current = filterKey;
28
+ observer.setFilter(options.filter ?? null);
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, [observer, filterKey]);
31
+
32
+ const optionsRef = useRef(options);
33
+ optionsRef.current = options;
34
+ useEffect(
35
+ () =>
36
+ subscribeNetworkRequestObserverEvents(observer, () => optionsRef.current),
37
+ [observer],
38
+ );
39
+
40
+ return observer;
41
+ }
@@ -0,0 +1,36 @@
1
+ import { ErrorBoundary, type JSX } from 'solid-js';
2
+ import { reportCaughtError } from '../core/report-caught-error';
3
+
4
+ export type IAppMetricsErrorBoundaryFallbackProps = {
5
+ /** The value the subtree threw, usually an `Error` but any value can be thrown */
6
+ error: unknown;
7
+ /** Clears the caught error and re-renders the children from a clean state */
8
+ resetError: () => void;
9
+ };
10
+
11
+ export type IAppMetricsErrorBoundaryFallback =
12
+ JSX.Element | ((props: IAppMetricsErrorBoundaryFallbackProps) => JSX.Element);
13
+
14
+ export type IAppMetricsErrorBoundaryProps = {
15
+ /** A JSX element, a render function receiving `error`/`resetError`, or `null` */
16
+ fallback: IAppMetricsErrorBoundaryFallback;
17
+ children: JSX.Element;
18
+ };
19
+
20
+ /** Records a render-phase error, renders `fallback` in place of the failed subtree */
21
+ export function AppMetricsErrorBoundary(
22
+ props: IAppMetricsErrorBoundaryProps,
23
+ ): JSX.Element {
24
+ return ErrorBoundary({
25
+ fallback: (error: unknown, resetError: () => void): JSX.Element => {
26
+ reportCaughtError(error);
27
+ const { fallback } = props;
28
+ return typeof fallback === 'function'
29
+ ? fallback({ error, resetError })
30
+ : fallback;
31
+ },
32
+ get children(): JSX.Element {
33
+ return props.children;
34
+ },
35
+ });
36
+ }
@@ -0,0 +1,29 @@
1
+ import { onMount, type JSX } from 'solid-js';
2
+ import { markFirstRender } from '../core';
3
+ import {
4
+ AppMetricsErrorBoundary,
5
+ type IAppMetricsErrorBoundaryFallback,
6
+ } from './app-metrics-error-boundary';
7
+
8
+ export type IAppMetricsRootProps = {
9
+ /** Omitting it leaves children unwrapped; pass `null` to capture without a fallback */
10
+ errorBoundaryFallback?: IAppMetricsErrorBoundaryFallback;
11
+ children: JSX.Element;
12
+ };
13
+
14
+ /** Marks the first render, so time-to-first-render is measured without a manual call */
15
+ export function AppMetricsRoot(props: IAppMetricsRootProps): JSX.Element {
16
+ onMount(() => markFirstRender());
17
+
18
+ // Only OMITTING the prop leaves children unwrapped - an explicit `null` still mounts the
19
+ // boundary and renders nothing, so the error is captured rather than propagating
20
+ if (props.errorBoundaryFallback === undefined) {
21
+ return props.children;
22
+ }
23
+ return AppMetricsErrorBoundary({
24
+ fallback: props.errorBoundaryFallback,
25
+ get children(): JSX.Element {
26
+ return props.children;
27
+ },
28
+ });
29
+ }
@@ -0,0 +1,12 @@
1
+ export * from '../core';
2
+ export {
3
+ AppMetricsErrorBoundary,
4
+ type IAppMetricsErrorBoundaryFallback,
5
+ type IAppMetricsErrorBoundaryFallbackProps,
6
+ type IAppMetricsErrorBoundaryProps,
7
+ } from './app-metrics-error-boundary';
8
+ export { AppMetricsRoot, type IAppMetricsRootProps } from './app-metrics-root';
9
+ export {
10
+ useNetworkRequestObserver,
11
+ type IUseNetworkRequestObserverOptions,
12
+ } from './use-network-request-observer';
@@ -0,0 +1,17 @@
1
+ import { createEffect, onCleanup } from 'solid-js';
2
+ import { createNetworkRequestObserverLifecycle } from '../core/network-request-observer-lifecycle';
3
+ import type { NetworkRequestObserver } from '../core';
4
+ import type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
5
+
6
+ export type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
7
+
8
+ /** Solid twin of `expo-app-metrics`'s `useNetworkRequestObserver` */
9
+ export function useNetworkRequestObserver(
10
+ getOptions: () => IUseNetworkRequestObserverOptions = () => ({}),
11
+ ): NetworkRequestObserver {
12
+ return createNetworkRequestObserverLifecycle(
13
+ getOptions,
14
+ createEffect,
15
+ onCleanup,
16
+ );
17
+ }
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ // Records a render-phase error and renders `fallback` in place of the failed subtree - the
3
+ // Svelte twin of ../react's AppMetricsErrorBoundary and ../vue's, over `<svelte:boundary>`
4
+ import { reportCaughtError } from '../core/report-caught-error';
5
+ import type { IAppMetricsErrorBoundaryProps } from './app-metrics-error-boundary-props';
6
+
7
+ let { fallback, children }: IAppMetricsErrorBoundaryProps = $props();
8
+ </script>
9
+
10
+ <svelte:boundary onerror={(error) => reportCaughtError(error)}>
11
+ {@render children()}
12
+ {#snippet failed(error, reset)}
13
+ {#if fallback}
14
+ {@render fallback({ error, resetError: reset })}
15
+ {/if}
16
+ {/snippet}
17
+ </svelte:boundary>
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ // Marks the first render so time-to-first-render is measured without a manual call, the
3
+ // Svelte twin of ../react's AppMetricsRoot and ../vue's
4
+ import { onMount } from 'svelte';
5
+ import { markFirstRender } from '../core';
6
+ import AppMetricsErrorBoundary from './AppMetricsErrorBoundary.svelte';
7
+ import type { IAppMetricsRootProps } from './app-metrics-root-props';
8
+
9
+ let { errorBoundaryFallback, children: rootChildren }: IAppMetricsRootProps =
10
+ $props();
11
+
12
+ onMount(() => markFirstRender());
13
+ </script>
14
+
15
+ {#if errorBoundaryFallback !== undefined}
16
+ <AppMetricsErrorBoundary fallback={errorBoundaryFallback}>
17
+ {@render rootChildren()}
18
+ </AppMetricsErrorBoundary>
19
+ {:else}
20
+ {@render rootChildren()}
21
+ {/if}
@@ -0,0 +1,17 @@
1
+ // Split out like adapters/svelte/src/components/view-props.ts: `tsc --build` sees a `.svelte`
2
+ // import only through the ambient default-export declaration, never a named type it re-exports
3
+
4
+ import type { Snippet } from 'svelte';
5
+
6
+ export type IAppMetricsErrorBoundaryFallbackProps = {
7
+ /** The value the subtree threw, usually an `Error` but any value can be thrown */
8
+ error: unknown;
9
+ /** Clears the caught error and re-renders the default slot from a clean state */
10
+ resetError: () => void;
11
+ };
12
+
13
+ export type IAppMetricsErrorBoundaryProps = {
14
+ /** A snippet receiving `error`/`resetError`, or `null` to capture without rendering */
15
+ fallback: Snippet<[IAppMetricsErrorBoundaryFallbackProps]> | null;
16
+ children: Snippet;
17
+ };
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { IAppMetricsErrorBoundaryFallbackProps } from './app-metrics-error-boundary-props';
3
+
4
+ export type IAppMetricsRootProps = {
5
+ /** Omitting it leaves children unwrapped; pass `null` to capture without a fallback */
6
+ errorBoundaryFallback?: Snippet<
7
+ [IAppMetricsErrorBoundaryFallbackProps]
8
+ > | null;
9
+ children: Snippet;
10
+ };
@@ -0,0 +1,10 @@
1
+ export * from '../core';
2
+ export type {
3
+ IAppMetricsErrorBoundaryFallbackProps,
4
+ IAppMetricsErrorBoundaryProps,
5
+ } from './app-metrics-error-boundary-props';
6
+ export type { IAppMetricsRootProps } from './app-metrics-root-props';
7
+ export { default as AppMetricsErrorBoundary } from './AppMetricsErrorBoundary.svelte';
8
+ export { default as AppMetricsRoot } from './AppMetricsRoot.svelte';
9
+ export { useNetworkRequestObserver } from './use-network-request-observer.svelte';
10
+ export type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
@@ -0,0 +1,7 @@
1
+ // Re-exports the existing harness instead of a third copy (packages/sqlite and
2
+ // packages/navigation each already carry one, not part of either package's public exports)
3
+ export {
4
+ createSvelteHarness,
5
+ loadComponent,
6
+ type ISvelteHarness,
7
+ } from '../../../sqlite/src/svelte/svelte-compile.test-helper';
@@ -0,0 +1,26 @@
1
+ // `$effect` is a compiler macro, can't be passed by reference like `watchEffect`/`createEffect`,
2
+ // so it's wrapped instead of handed to `createNetworkRequestObserverLifecycle` directly
3
+
4
+ import { createNetworkRequestObserverLifecycle } from '../core/network-request-observer-lifecycle';
5
+ import type { NetworkRequestObserver } from '../core';
6
+ import type { IUseNetworkRequestObserverOptions } from '../core/network-request-observer-lifecycle';
7
+
8
+ export function useNetworkRequestObserver(
9
+ getOptions: () => IUseNetworkRequestObserverOptions = () => ({}),
10
+ ): NetworkRequestObserver {
11
+ const cleanups: Array<() => void> = [];
12
+
13
+ const observer = createNetworkRequestObserverLifecycle(
14
+ getOptions,
15
+ effect => {
16
+ $effect(effect);
17
+ },
18
+ cleanup => cleanups.push(cleanup),
19
+ );
20
+
21
+ $effect(() => () => {
22
+ for (const cleanup of cleanups) cleanup();
23
+ });
24
+
25
+ return observer;
26
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ defineComponent,
3
+ onErrorCaptured,
4
+ shallowRef,
5
+ type VNode,
6
+ } from '@vue/runtime-core';
7
+ import { reportCaughtError } from '../core/report-caught-error';
8
+
9
+ export type IAppMetricsErrorBoundaryFallbackProps = {
10
+ /** The value the subtree threw, usually an `Error` but any value can be thrown */
11
+ error: unknown;
12
+ /** Clears the caught error and re-renders the default slot from a clean state */
13
+ resetError: () => void;
14
+ };
15
+
16
+ export type IAppMetricsErrorBoundaryFallback =
17
+ | VNode
18
+ | null
19
+ | ((props: IAppMetricsErrorBoundaryFallbackProps) => VNode | null);
20
+
21
+ export type IAppMetricsErrorBoundaryProps = {
22
+ /** A VNode, a render function receiving `error`/`resetError`, or `null` */
23
+ fallback: IAppMetricsErrorBoundaryFallback;
24
+ };
25
+
26
+ /** Records a render-phase error, renders `fallback` in place of the default slot */
27
+ export const AppMetricsErrorBoundary =
28
+ defineComponent<IAppMetricsErrorBoundaryProps>(
29
+ (props, { slots }) => {
30
+ const hasError = shallowRef(false);
31
+ const caughtError = shallowRef<unknown>(null);
32
+
33
+ // Returning `false` stops propagation, keeping the error off this engine's own
34
+ // uncaught-error channel, same asymmetry as adapters/vue's own render errors
35
+ onErrorCaptured(error => {
36
+ hasError.value = true;
37
+ caughtError.value = error;
38
+ reportCaughtError(error);
39
+ return false;
40
+ });
41
+
42
+ function resetError(): void {
43
+ hasError.value = false;
44
+ caughtError.value = null;
45
+ }
46
+
47
+ return (): VNode | VNode[] | null | undefined => {
48
+ if (!hasError.value) return slots.default?.();
49
+ const { fallback } = props;
50
+ return typeof fallback === 'function'
51
+ ? fallback({ error: caughtError.value, resetError })
52
+ : fallback;
53
+ };
54
+ },
55
+ { name: 'AppMetricsErrorBoundary', props: ['fallback'] },
56
+ );
@@ -0,0 +1,32 @@
1
+ import { defineComponent, h, onMounted, type VNode } from '@vue/runtime-core';
2
+ import { markFirstRender } from '../core';
3
+ import {
4
+ AppMetricsErrorBoundary,
5
+ type IAppMetricsErrorBoundaryFallback,
6
+ } from './app-metrics-error-boundary';
7
+
8
+ export type IAppMetricsRootProps = {
9
+ /** Omitting it leaves the default slot unwrapped; pass `null` to capture without rendering */
10
+ errorBoundaryFallback?: IAppMetricsErrorBoundaryFallback;
11
+ };
12
+
13
+ /** Marks the first render, so time-to-first-render is measured without a manual call */
14
+ export const AppMetricsRoot = defineComponent<IAppMetricsRootProps>(
15
+ (props, { slots }) => {
16
+ onMounted(() => markFirstRender());
17
+
18
+ return (): VNode | VNode[] | null | undefined => {
19
+ // Only OMITTING the prop leaves the slot unwrapped - an explicit `null` still mounts the
20
+ // boundary and renders nothing, so the error is captured rather than propagating
21
+ if (props.errorBoundaryFallback !== undefined) {
22
+ return h(
23
+ AppMetricsErrorBoundary,
24
+ { fallback: props.errorBoundaryFallback },
25
+ slots.default,
26
+ );
27
+ }
28
+ return slots.default?.();
29
+ };
30
+ },
31
+ { name: 'AppMetricsRoot', props: ['errorBoundaryFallback'] },
32
+ );