Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/native-component-list/src/screens/AppMetricsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useFocusEffect } from '@react-navigation/native';
import { useTheme } from 'ThemeProvider';
import AppMetrics, { type MainSession, type Metric } from 'expo-app-metrics';
import AppMetrics, { type Metric } from 'expo-app-metrics';
import * as React from 'react';
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';

export default function AppMetricsScreen() {
const { theme } = useTheme();
const [session, setSession] = React.useState<MainSession | null>(null);
const [metrics, setMetrics] = React.useState<Metric[]>([]);
const [refreshing, setRefreshing] = React.useState(false);

useFocusEffect(
React.useCallback(() => {
let cancelled = false;
AppMetrics.getMainSession().then((s) => {
if (!cancelled) setSession(s);
if (!cancelled) setMetrics(s?.metrics ?? []);
});
return () => {
cancelled = true;
Expand All @@ -24,13 +24,13 @@ export default function AppMetricsScreen() {
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
try {
setSession(await AppMetrics.getMainSession());
setMetrics((await AppMetrics.getMainSession())?.metrics ?? []);
} finally {
setRefreshing(false);
}
}, []);

const navMetrics: Metric[] = (session?.metrics ?? []).filter((m) => m.category === 'navigation');
const navMetrics: Metric[] = metrics.filter((m) => m.category === 'navigation');

return (
<ScrollView
Expand Down
7 changes: 7 additions & 0 deletions apps/observe-tester/app/(tabs)/(metrics)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export default function Index() {
const { markInteractive } = useObserve();

async function handleMarkInteractive() {
// Fire a network request first so it lands inside the launch→TTI window and shows up in the
// TTI metric's expo.network.requests.* params. Failures are swallowed — this is exercise code.
try {
await fetch('https://expo.dev');
} catch {
// ignore
}
await markInteractive();
}

Expand Down
1 change: 1 addition & 0 deletions apps/observe-tester/app/(tabs)/(sessions)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function SessionsLayout() {
headerTintColor: theme.text.default,
}}>
<Stack.Screen name="sessions/index" options={{ title: 'Sessions' }} />
<Stack.Screen name="sessions/main" options={{ title: 'Main session' }} />
<Stack.Screen name="sessions/[id]" options={{ title: 'Session' }} />
</Stack>
);
Expand Down
152 changes: 11 additions & 141 deletions apps/observe-tester/app/(tabs)/(sessions)/sessions/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,27 @@
import AppMetrics, { type Session } from 'expo-app-metrics';
import { useObserve } from 'expo-observe';
import { Stack, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
import { Platform, Pressable, ScrollView, StyleSheet, Text } from 'react-native';
import { useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useState } from 'react';

import { CallStackTreeView } from '@/components/CallStackTreeView';
import { Chevron } from '@/components/Chevron';
import { CrashReportPanel } from '@/components/CrashReportPanel';
import { Divider } from '@/components/Divider';
import { JSONView } from '@/components/JSONView';
import { LogsPanel } from '@/components/LogsPanel';
import { MetricsFilter } from '@/components/MetricsFilter';
import { MetricsPanel } from '@/components/MetricsPanel';
import { SessionHeader } from '@/components/SessionHeader';
import { useTheme } from '@/utils/theme';
import { SessionDetail } from '@/components/SessionDetail';

export default function SessionDetail() {
export default function InactiveSessionScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const theme = useTheme();
const [session, setSession] = useState<Session | null>(null);
const [loaded, setLoaded] = useState(false);
const [showRawJson, setShowRawJson] = useState(false);
const [selectedMetricNames, setSelectedMetricNames] = useState<Set<string>>(() => new Set());

// Seed the selection to every distinct metric name once per session id. Keyed on `session?.id`
// so refocusing the tab (which reloads sessions and creates a new metrics array reference)
// doesn't wipe the user's filter.
useEffect(() => {
if (!session) return;
const names = new Set<string>();
for (const metric of session.metrics) names.add(metric.name);
setSelectedMetricNames(names);
}, [session?.id]);

const { markInteractive } = useObserve();
useEffect(() => {
setTimeout(() => {
markInteractive();
}, 100);
}, []);

useFocusEffect(
useCallback(() => {
AppMetrics.getAllSessions().then((sessions) => {
let cancelled = false;
AppMetrics.getInactiveSessions().then((sessions) => {
if (cancelled) return;
setSession(sessions.find((s) => s.id === id) ?? null);
setLoaded(true);
});
return () => {
cancelled = true;
};
}, [id])
);

return (
<ScrollView
style={[styles.container, { backgroundColor: theme.background.screen }]}
contentContainerStyle={styles.contentContainer}>
<Stack.Screen options={{ title: id?.slice(0, 8) ?? 'Session' }} />
{!loaded ? null : session ? (
<>
<SessionHeader session={session} />
<Divider style={styles.divider} />
{session.type === 'main' && session.crashReport ? (
<>
<Text style={[styles.sectionTitle, { color: theme.text.default }]}>Crash report</Text>
<CrashReportPanel report={session.crashReport} />
{session.crashReport.callStackTree ? (
<>
<Divider style={styles.divider} />
<Text style={[styles.sectionTitle, { color: theme.text.default }]}>
Call stacks
</Text>
<CallStackTreeView tree={session.crashReport.callStackTree} />
</>
) : null}
<Divider style={styles.divider} />
</>
) : null}
<Text style={[styles.sectionTitle, { color: theme.text.default }]}>Metrics</Text>
<MetricsFilter
metrics={session.metrics}
selected={selectedMetricNames}
onChange={setSelectedMetricNames}
/>
<MetricsPanel metrics={session.metrics} filter={selectedMetricNames} />
<Divider style={styles.divider} />
<Text style={[styles.sectionTitle, { color: theme.text.default }]}>Log events</Text>
<LogsPanel logs={session.logs} />
<Divider style={styles.divider} />
<Pressable
onPress={() => setShowRawJson((v) => !v)}
style={({ pressed }) => [
styles.rawJsonHeader,
showRawJson && styles.rawJsonHeaderExpanded,
pressed && { opacity: 0.6 },
]}>
<Text style={[styles.sectionTitle, styles.rawJsonTitle, { color: theme.text.default }]}>
Raw JSON
</Text>
<Chevron expanded={showRawJson} />
</Pressable>
{showRawJson ? <JSONView value={stripCallStackTree(session)} /> : null}
</>
) : (
<Text style={[styles.notFound, { color: theme.text.default }]}>Session not found</Text>
)}
</ScrollView>
);
}

// The call stack tree balloons the raw JSON to the point where it can fail to render. It's
// already shown visually in the "Call stacks" section above, so we omit it from the raw JSON
// and replace it with a marker noting that.
function stripCallStackTree(session: Session): Session {
if (session.type !== 'main' || !session.crashReport?.callStackTree) {
return session;
}
return {
...session,
crashReport: {
...session.crashReport,
callStackTree: '<omitted, see Call stacks section>' as never,
},
};
return <SessionDetail session={session} loaded={loaded} title={id?.slice(0, 8) ?? 'Session'} />;
}

const styles = StyleSheet.create({
container: {
padding: 20,
},
contentContainer: {
paddingBottom: Platform.select({ ios: 30, android: 150 }),
},
notFound: {
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
marginBottom: 12,
},
divider: {
marginVertical: 16,
},
rawJsonHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
},
rawJsonTitle: {
marginBottom: 0,
},
rawJsonHeaderExpanded: {
marginBottom: 12,
},
});
Loading
Loading