);
}
}
return child;
});
return (
{childrenWithNavigationTracking}
);
}
================================================
FILE: src/components/PerformanceOverlay.tsx
================================================
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useMetricsStore } from '../store/metricsStore';
export function PerformanceOverlay() {
const { traces, fps, currentScreen } = useMetricsStore();
return (
Performance Metrics
FPS: {fps ? `${fps.toFixed(1)}` : 'N/A'}
Screen: {currentScreen || 'N/A'}
Traces: {traces.length}
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 10,
borderRadius: 5,
},
title: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 5,
},
metric: {
color: '#fff',
fontSize: 14,
marginBottom: 3,
},
});
================================================
FILE: src/core/initOptic.ts
================================================
import { initRenderTracking } from '../metrics/globalRenderTracking';
import { initNetworkTracking } from '../metrics/network';
import { useMetricsStore } from '../store/metricsStore';
import { trackStartupTime } from '../metrics/startup';
import { setOpticEnabled } from '../store/metricsStore';
import React from 'react';
export interface InitOpticOptions {
enabled?: boolean;
onMetricsLogged?: (metrics: any) => void;
network?: boolean;
startup?: boolean;
reRenders?: boolean;
traces?: boolean;
}
export interface OpticConfig {
enabled: boolean;
onMetricsLogged?: (metrics: any) => void;
network: boolean;
startup: boolean;
reRenders: boolean;
traces: boolean;
}
// Create a wrapper component that automatically tracks screen names
function withScreenTracking(WrappedComponent: React.ComponentType
) {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Unknown';
const screenName = displayName.replace(/Screen$/, '');
function WithScreenTracking(props: P) {
const setCurrentScreen = useMetricsStore((state) => state.setCurrentScreen);
React.useEffect(() => {
setCurrentScreen(screenName);
return () => setCurrentScreen(null);
}, [setCurrentScreen]);
return React.createElement(WrappedComponent, props);
}
WithScreenTracking.displayName = `WithScreenTracking(${displayName})`;
return WithScreenTracking;
}
// Function to check if a component is likely a screen
function isScreenComponent(component: any): boolean {
const name = component.displayName || component.name || '';
return name.endsWith('Screen') || name.endsWith('Page') || name.endsWith('View');
}
// Store to keep track of wrapped components
const wrappedComponents = new WeakMap();
// Function to wrap a component if it's a screen
function wrapIfScreen
(Component: React.ComponentType
): React.ComponentType
{
if (!isScreenComponent(Component)) {
return Component;
}
// Check if already wrapped
if (wrappedComponents.has(Component)) {
return wrappedComponents.get(Component);
}
// Wrap the component
const wrapped = withScreenTracking(Component);
wrappedComponents.set(Component, wrapped);
return wrapped;
}
export function initOptic(options: InitOpticOptions = {}) {
const {
enabled = true,
onMetricsLogged,
network = true,
startup = true,
reRenders = true,
traces = true,
} = options;
const config: OpticConfig = {
enabled,
onMetricsLogged,
network,
startup,
reRenders,
traces,
};
setOpticEnabled(enabled);
if (!enabled) {
// Do not initialize anything if disabled
return;
}
// Initialize render tracking if enabled
if (reRenders) {
initRenderTracking();
}
// Initialize network tracking if enabled
if (network) {
initNetworkTracking();
}
// Track startup time if enabled
if (startup) {
trackStartupTime();
}
// Initialize metrics store
useMetricsStore.getState();
// Subscribe to metrics changes and call the callback
if (onMetricsLogged) {
const unsubscribe = useMetricsStore.subscribe((metrics) => {
onMetricsLogged(metrics);
});
// Optionally return unsubscribe so the user can clean up
return {
config,
unsubscribe,
};
}
return config;
}
================================================
FILE: src/hoc/withScreenTracking.tsx
================================================
import React, { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useMetricsStore } from '../store/metricsStore';
export function withScreenTracking
(
WrappedComponent: React.ComponentType
,
screenName?: string
) {
return function WithScreenTracking(props: P) {
const navigation = useNavigation();
const setCurrentScreen = useMetricsStore((state) => state.setCurrentScreen);
useEffect(() => {
// Get screen name from navigation state or use provided name
const route = navigation.getState().routes[navigation.getState().index];
const currentScreenName = screenName || route.name;
setCurrentScreen(currentScreenName);
return () => {
// Clear screen name when component unmounts
setCurrentScreen(null);
};
}, [navigation, screenName, setCurrentScreen]);
return ;
};
}
================================================
FILE: src/hooks/useAutoScreenName.ts
================================================
import { useEffect } from 'react';
import { useMetricsStore } from '../store/metricsStore';
/**
* Automatically tracks the current screen name based on the component's name.
* Just add this hook to your screen components without any parameters:
*
* @example
* function HomeScreen() {
* useAutoScreenName(); // That's it! It will automatically use "HomeScreen" as the name
* return ...;
* }
*/
export function useAutoScreenName() {
const setCurrentScreen = useMetricsStore((state) => state.setCurrentScreen);
useEffect(() => {
// Get the component name from the stack trace
const stack = new Error().stack || '';
const match = stack.match(/at\s+(\w+)\s+\(/);
const componentName = match ? match[1] : 'Unknown';
// Remove "Screen" suffix if present
const screenName = componentName.replace(/Screen$/, '');
setCurrentScreen(screenName);
return () => setCurrentScreen(null);
}, [setCurrentScreen]);
}
================================================
FILE: src/hooks/useScreenName.ts
================================================
import { useEffect } from 'react';
import { useMetricsStore } from '../store/metricsStore';
/**
* A simple hook to track screen names in your React Native app.
* Just add this hook to your screen components:
*
* @example
* function HomeScreen() {
* useScreenName('Home');
* return ...;
* }
*/
export function useScreenName(screenName: string) {
const setCurrentScreen = useMetricsStore((state) => state.setCurrentScreen);
useEffect(() => {
setCurrentScreen(screenName);
return () => setCurrentScreen(null);
}, [screenName, setCurrentScreen]);
}
================================================
FILE: src/index.ts
================================================
export { initOptic } from './core/initOptic';
export { OpticProvider } from './providers/OpticProvider';
export { useMetricsStore } from './store/metricsStore';
export { useRenderMonitor } from './metrics/reRenders';
export { startTrace, endTrace } from './metrics/trace';
export type { InitOpticOptions } from './core/initOptic';
================================================
FILE: src/index.tsx
================================================
import { OpticProvider } from './components/OpticProvider';
import { initOptic } from './core/initOptic';
import { Overlay } from './overlay/Overlay';
import { useRenderMonitor } from './metrics/reRenders';
import { useScreenMetrics } from './metrics/screen';
export {
initOptic,
Overlay,
useRenderMonitor,
useScreenMetrics,
OpticProvider
};
================================================
FILE: src/metrics/fps.ts
================================================
import { useMetricsStore } from '../store/metricsStore';
export interface FPSMetrics {
fps: number;
timestamp: number;
}
export class FPSManager {
private frameCount: number = 0;
private lastTime: number = 0;
private animationFrameId: number | null = null;
private readonly updateInterval: number = 1000; // Update FPS every second
constructor() {
this.lastTime = performance.now();
}
private updateFPS = () => {
const currentTime = performance.now();
const elapsed = currentTime - this.lastTime;
if (elapsed >= this.updateInterval) {
const fps = Math.round((this.frameCount * 1000) / elapsed);
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
if (currentScreen) {
metricsStore.setFPS(fps, currentScreen);
}
this.frameCount = 0;
this.lastTime = currentTime;
}
this.frameCount++;
this.animationFrameId = requestAnimationFrame(this.updateFPS);
};
public startTracking = () => {
if (!this.animationFrameId) {
this.lastTime = performance.now();
this.frameCount = 0;
this.animationFrameId = requestAnimationFrame(this.updateFPS);
}
};
public stopTracking = () => {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
};
}
export const getFPSColor = (fps: number): string => {
if (fps >= 55) return '#4CAF50'; // Good (green)
if (fps >= 30) return '#FFC107'; // Warning (yellow)
return '#F44336'; // Poor (red)
};
================================================
FILE: src/metrics/globalRenderTracking.ts
================================================
import * as React from 'react';
import { useMetricsStore } from '../store/metricsStore';
declare global {
var __OPTIC_ROOT_COMPONENT__: React.ComponentType | undefined;
var __OPTIC_RENDER_TRACKING_ENABLED__: boolean;
}
// Store to keep track of component renders
const renderCounts: Record = {};
// Create a wrapper component that tracks renders
const withRenderTracking = (WrappedComponent: React.ComponentType) => {
const RenderTrackingWrapper: React.FC = (props) => {
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Unknown';
const incrementReRender = useMetricsStore((state) => state.incrementReRender);
React.useEffect(() => {
if (global.__OPTIC_RENDER_TRACKING_ENABLED__) {
const reRenderInfo = {
componentName,
timestamp: Date.now(),
changedProps: props,
renderCount: (renderCounts[componentName] || 0) + 1
};
incrementReRender(componentName, reRenderInfo);
renderCounts[componentName] = (renderCounts[componentName] || 0) + 1;
}
});
return React.createElement(WrappedComponent, props);
};
return RenderTrackingWrapper;
};
// Function to wrap any component with render tracking
export function wrapWithRenderTracking>(
component: T
): T {
if (!component) return component;
// Skip if already wrapped
if ((component as any).__OPTIC_WRAPPED__) return component;
const wrapped = withRenderTracking(component);
(wrapped as any).__OPTIC_WRAPPED__ = true;
return wrapped as T;
}
// Function to enable/disable render tracking
export function setRenderTrackingEnabled(enabled: boolean) {
global.__OPTIC_RENDER_TRACKING_ENABLED__ = enabled;
}
// Function to wrap the root component
export function setupGlobalRenderTracking() {
// Get the root component
const rootComponent = global.__OPTIC_ROOT_COMPONENT__;
if (!rootComponent) {
return;
}
// Wrap the root component with render tracking
const wrappedRoot = wrapWithRenderTracking(rootComponent);
global.__OPTIC_ROOT_COMPONENT__ = wrappedRoot;
}
// Function to set the root component
export function setRootComponent(component: React.ComponentType) {
if (!component) return;
global.__OPTIC_ROOT_COMPONENT__ = component;
// If render tracking is enabled, wrap the component
if (global.__OPTIC_RENDER_TRACKING_ENABLED__) {
setupGlobalRenderTracking();
}
}
// Initialize render tracking
export function initRenderTracking() {
// Set initial state
global.__OPTIC_RENDER_TRACKING_ENABLED__ = true;
// Wrap the root component if it exists
if (global.__OPTIC_ROOT_COMPONENT__) {
setupGlobalRenderTracking();
}
}
================================================
FILE: src/metrics/network.ts
================================================
import { useMetricsStore } from '../store/metricsStore';
// Network performance thresholds (in milliseconds)
const NETWORK_THRESHOLDS = {
GOOD: 200,
WARNING: 500,
CRITICAL: 1000,
};
let originalFetch: typeof fetch | null = null;
let pendingRequests = new Map();
const formatDuration = (duration: number): string => {
if (duration >= 1000) {
return `${(duration / 1000).toFixed(1)}s`;
}
return `${duration}ms`;
};
export const initNetworkTracking = () => {
if (originalFetch !== null) return; // Already initialized
try {
originalFetch = global.fetch;
global.fetch = async function (input: RequestInfo | URL, init?: RequestInit) {
const startTime = Date.now();
const url = input instanceof Request ? input.url : input.toString();
const method = input instanceof Request ? input.method : (init?.method || 'GET');
// Store the request start time
pendingRequests.set(url, { startTime, url, method });
try {
const response = await originalFetch!(input, init);
const responseTime = Date.now();
const responseDuration = responseTime - startTime;
// Clone the response to ensure we can read the body
const clonedResponse = response.clone();
// Create a new response that will track when the body is read
const newResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
// Override the json and text methods to track completion
const originalJson = newResponse.json;
const originalText = newResponse.text;
newResponse.json = async function() {
try {
// First try to read the cloned response to ensure it's valid JSON
await clonedResponse.json();
// If we get here, the JSON is valid, so read the actual response
const data = await originalJson.call(this);
const endTime = Date.now();
const totalDuration = endTime - startTime;
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
const networkRequest = {
url,
method,
duration: totalDuration,
responseDuration,
status: response.status,
screen: currentScreen,
timestamp: endTime,
startTime,
endTime,
};
metricsStore.addNetworkRequest(networkRequest);
pendingRequests.delete(url);
return data;
} catch (error) {
const endTime = Date.now();
const totalDuration = endTime - startTime;
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
const networkRequest = {
url,
method,
duration: totalDuration,
responseDuration,
status: response.status,
screen: currentScreen,
timestamp: endTime,
startTime,
endTime,
error: error instanceof Error ? error.message : 'Unknown error',
};
metricsStore.addNetworkRequest(networkRequest);
pendingRequests.delete(url);
throw error;
}
};
newResponse.text = async function() {
try {
const data = await originalText.call(this);
const endTime = Date.now();
const totalDuration = endTime - startTime;
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
const networkRequest = {
url,
method,
duration: totalDuration,
responseDuration,
status: response.status,
screen: currentScreen,
timestamp: endTime,
startTime,
endTime,
};
metricsStore.addNetworkRequest(networkRequest);
pendingRequests.delete(url);
return data;
} catch (error) {
const endTime = Date.now();
const totalDuration = endTime - startTime;
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
const networkRequest = {
url,
method,
duration: totalDuration,
responseDuration,
status: response.status,
screen: currentScreen,
timestamp: endTime,
startTime,
endTime,
error: error instanceof Error ? error.message : 'Unknown error',
};
metricsStore.addNetworkRequest(networkRequest);
pendingRequests.delete(url);
throw error;
}
};
return newResponse;
} catch (error) {
const endTime = Date.now();
const totalDuration = endTime - startTime;
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
const networkRequest = {
url,
method,
duration: totalDuration,
status: 0,
screen: currentScreen,
timestamp: endTime,
startTime,
endTime,
error: error instanceof Error ? error.message : 'Unknown error',
};
metricsStore.addNetworkRequest(networkRequest);
pendingRequests.delete(url);
throw error;
}
};
} catch (error) {
if (originalFetch) {
global.fetch = originalFetch;
originalFetch = null;
}
}
};
export const stopNetworkTracking = () => {
if (originalFetch === null) return;
global.fetch = originalFetch;
originalFetch = null;
pendingRequests.clear();
};
export const getNetworkColor = (duration: number | null | undefined): string => {
if (duration === null || duration === undefined) return '#666666';
if (duration <= NETWORK_THRESHOLDS.GOOD) return '#4CAF50';
if (duration <= NETWORK_THRESHOLDS.WARNING) return '#FFC107';
return '#F44336';
};
export const getLatestNetworkRequest = () => {
const metricsStore = useMetricsStore.getState();
const currentScreen = metricsStore.currentScreen;
const networkRequests = metricsStore.networkRequests;
const screenNetworkRequests = networkRequests.filter(req => req.screen === currentScreen);
return screenNetworkRequests[screenNetworkRequests.length - 1];
};
================================================
FILE: src/metrics/reRenders.ts
================================================
import React, { useEffect, useRef } from 'react';
import { useMetricsStore } from '../store/metricsStore';
interface ReRenderInfo {
componentName: string;
timestamp: number;
changedProps: Record;
renderCount: number;
stackTrace?: string;
}
/**
* Hook to monitor and log prop changes for a component.
* @param componentName Name of the component
* @param props Component props
* @param options Additional options for tracking
*/
export function useRenderMonitor>(
componentName: string,
props: T,
options: {
debug?: boolean;
ignoreProps?: string[];
trackStack?: boolean;
} = {}
) {
if (!React) return;
const { ignoreProps = [], trackStack = false } = options;
const prevProps = useRef(null);
const renderCount = useRef(0);
const incrementReRender = useMetricsStore((state) => state.incrementReRender);
const currentScreen = useMetricsStore((state) => state.currentScreen);
useEffect(() => {
prevProps.current = null;
renderCount.current = 0;
}, [currentScreen]);
useEffect(() => {
if (prevProps.current) {
const changedProps: Record = {};
for (const key of Object.keys(props)) {
if (!ignoreProps.includes(key) && prevProps.current[key] !== props[key]) {
changedProps[key] = {
from: prevProps.current[key],
to: props[key],
};
}
}
if (Object.keys(changedProps).length > 0) {
renderCount.current++;
const reRenderInfo: ReRenderInfo = {
componentName,
timestamp: Date.now(),
changedProps,
renderCount: renderCount.current,
};
if (trackStack) {
reRenderInfo.stackTrace = new Error().stack;
}
incrementReRender(componentName);
}
}
prevProps.current = props;
});
}
let renderTrackingSetup = false;
/**
* Sets up global render tracking with configuration options.
* @param options Configuration options for render tracking
*/
export function setupRenderTracking(options: {
debug?: boolean;
trackStack?: boolean;
} = {}) {
if (!renderTrackingSetup) {
renderTrackingSetup = true;
}
}
================================================
FILE: src/metrics/screen.ts
================================================
import { useEffect, useRef, useCallback } from 'react';
import { useMetricsStore } from '../store/metricsStore';
/**
* Hook to track screen performance metrics.
* @param screenName Name of the current screen
*/
export function useScreenMetrics(screenName: string) {
const setCurrentScreen = useMetricsStore((state) => state.setCurrentScreen);
const screens = useMetricsStore((state) => state.screens);
const prevScreenRef = useRef(null);
const mountedRef = useRef(true);
// Memoize the screen change handler
const handleScreenChange = useCallback(() => {
const isNewScreen = prevScreenRef.current !== screenName;
if (isNewScreen) {
prevScreenRef.current = screenName;
setCurrentScreen(screenName);
}
}, [screenName, setCurrentScreen]);
// Handle screen changes
useEffect(() => {
handleScreenChange();
}, [handleScreenChange]);
// Handle cleanup
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, [screenName]);
}
================================================
FILE: src/metrics/startup.ts
================================================
export {};
import { useMetricsStore } from '../store/metricsStore';
// Global app start time (should be set as early as possible in the app entrypoint)
declare global {
var __OPTIC_APP_START_TIME__: number | undefined;
var __OPTIC_STARTUP_CAPTURED__: boolean;
}
if (global.__OPTIC_APP_START_TIME__ === undefined) {
global.__OPTIC_APP_START_TIME__ = Date.now();
}
if (global.__OPTIC_STARTUP_CAPTURED__ === undefined) {
global.__OPTIC_STARTUP_CAPTURED__ = false;
}
/**
* Measures time since global app start and logs it to the console.
* Only measures once and stores the result.
*/
export function trackStartupTime() {
// Only measure startup time once
if (global.__OPTIC_STARTUP_CAPTURED__) {
return;
}
const start = global.__OPTIC_APP_START_TIME__ || Date.now();
// Use requestAnimationFrame to ensure we measure after initial render
requestAnimationFrame(() => {
if (!global.__OPTIC_STARTUP_CAPTURED__) {
const duration = Date.now() - start;
// Mark as captured before setting the time to prevent race conditions
global.__OPTIC_STARTUP_CAPTURED__ = true;
useMetricsStore.getState().setStartupTime(duration);
}
});
}
================================================
FILE: src/metrics/trace.ts
================================================
import { useMetricsStore } from '../store/metricsStore';
interface Trace {
interactionName: string;
componentName: string;
duration: number;
timestamp: number;
}
class TraceManager {
private activeTraces: Map = new Map();
private traces: Trace[] = [];
private readonly MAX_TRACES = 10;
/**
* Start tracing an interaction
* @param interactionName Name of the interaction (e.g., 'OpenModal')
*/
startTrace(interactionName: string) {
if (!__DEV__) return;
this.activeTraces.set(interactionName, Date.now());
}
/**
* End tracing and record the duration
* @param interactionName Name of the interaction
* @param componentName Name of the component that rendered
*/
endTrace(interactionName: string, componentName: string) {
if (!__DEV__) return;
const startTime = this.activeTraces.get(interactionName);
if (!startTime) return;
const duration = Date.now() - startTime;
const trace: Trace = {
interactionName,
componentName,
duration,
timestamp: Date.now()
};
this.traces.unshift(trace);
if (this.traces.length > this.MAX_TRACES) {
this.traces.pop();
}
useMetricsStore.getState().setTrace(trace);
this.activeTraces.delete(interactionName);
}
/**
* Get all traces
*/
getTraces(): Trace[] {
return [...this.traces];
}
/**
* Clear all traces
*/
clearTraces() {
this.traces = [];
this.activeTraces.clear();
}
}
export const traceManager = new TraceManager();
// Export the public API
export const startTrace = traceManager.startTrace.bind(traceManager);
export const endTrace = traceManager.endTrace.bind(traceManager);
================================================
FILE: src/overlay/Overlay.tsx
================================================
import React, { useRef, useState } from 'react';
import { View, Text, StyleSheet, PanResponder, Animated, Dimensions, TouchableOpacity, Clipboard, Image, Platform, Linking, ScrollView } from 'react-native';
import { useMetricsStore } from '../store/metricsStore';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { getFPSColor } from '../metrics/fps';
import { getNetworkColor, getLatestNetworkRequest } from '../metrics/network';
import { opticEnabled } from '../store/metricsStore';
const minimizeImageUrl = 'https://img.icons8.com/material-rounded/24/ffffff/minus.png';
const maximizeImageUrl = 'https://img.icons8.com/ios-filled/50/ffffff/full-screen.png';
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
const METRICS_THRESHOLDS = {
STARTUP: {
good: 1000, // 1 second
warning: 2000, // 2 seconds
},
TRACE: {
good: 50, // 50ms
warning: 200, // 200ms
},
FPS: {
good: 55, // 55+ FPS is good
warning: 30, // 30+ FPS is acceptable
},
};
const getMetricColor = (metric: 'STARTUP' | 'TRACE' | 'FPS', value: number) => {
const thresholds = METRICS_THRESHOLDS[metric];
if (metric === 'FPS') {
if (value >= thresholds.good) return '#4CAF50';
if (value >= thresholds.warning) return '#FFC107';
return '#F44336';
}
if (value <= thresholds.good) return '#4CAF50';
if (value <= thresholds.warning) return '#FFC107';
return '#F44336';
};
const getStatusColor = (status: number): string => {
if (status >= 200 && status < 300) return '#4CAF50'; // Green for success
if (status >= 400) return '#F44336'; // Red for client/server errors
return '#FFC107'; // Yellow for other status codes
};
export const Overlay: React.FC = () => {
if (!opticEnabled) return null;
const insets = useSafeAreaInsets();
const currentScreen = useMetricsStore((state) => state.currentScreen);
const screens = useMetricsStore((state) => state.screens);
const startupTime = useMetricsStore((state) => state.startupTime);
const networkRequests = useMetricsStore((state) => state.networkRequests);
const traces = useMetricsStore((state) => state.traces);
const [isMinimized, setIsMinimized] = useState(false);
const [isNetworkExpanded, setIsNetworkExpanded] = useState(false);
const [isTracesExpanded, setIsTracesExpanded] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const [expanded, setExpanded] = useState(false);
const [expandedTrace, setExpandedTrace] = useState(false);
const pan = useRef(new Animated.ValueXY()).current;
const [position, setPosition] = useState({
x: (SCREEN_WIDTH - 300) / 2,
y: insets.top + 20
});
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (_, gesture) => {
const newX = position.x + gesture.dx;
const newY = position.y + gesture.dy;
// Keep within screen bounds with padding
const boundedX = Math.max(10, Math.min(newX, SCREEN_WIDTH - 290));
const boundedY = Math.max(insets.top + 10, Math.min(newY, SCREEN_HEIGHT - 200));
// Update position directly without animation
setPosition({ x: boundedX, y: boundedY });
},
onPanResponderRelease: () => {
// Reset the pan value without animation
pan.setValue({ x: 0, y: 0 });
},
})
).current;
const currentScreenMetrics = currentScreen ? screens[currentScreen] : null;
const latestRequest = getLatestNetworkRequest();
const latestTrace = traces[traces.length - 1];
const handleCopyMetrics = () => {
try {
const metrics = {
currentScreen: currentScreen || 'No Screen',
startupTime: startupTime ? `${startupTime.toFixed(2)}ms` : 'N/A',
fps: currentScreenMetrics?.fps ? `${currentScreenMetrics.fps.toFixed(1)} FPS` : 'N/A',
latestNetworkRequest: latestRequest ? {
url: latestRequest.url,
duration: `${latestRequest.duration.toFixed(2)}ms`,
status: latestRequest.status
} : 'N/A',
latestTrace: latestTrace ? {
interactionName: latestTrace.interactionName,
componentName: latestTrace.componentName,
duration: `${latestTrace.duration.toFixed(2)}ms`,
} : 'N/A',
};
// Use Clipboard API instead of console.log
if (Platform.OS === 'ios' || Platform.OS === 'android') {
Clipboard.setString(JSON.stringify(metrics, null, 2));
}
} catch (error) {
console.error('Error copying metrics:', error);
}
};
const handleOpenWebsite = () => {
Linking.openURL('https://useoptic.dev');
};
const renderCollapsedView = () => (
🚀 {startupTime !== null ? `${startupTime.toFixed(1)}ms` : '...'}
🎮 {currentScreenMetrics?.fps !== null && currentScreenMetrics?.fps !== undefined ? `${currentScreenMetrics.fps.toFixed(1)}` : '...'}
);
if (!currentScreen) return null;
return (
setIsCollapsed(!isCollapsed)}
/>
{isCollapsed ? (
renderCollapsedView()
) : (
<>
Performance Metrics
setIsMinimized(!isMinimized)}
>
{currentScreen || 'No Screen'}
{!isMinimized && (
Performance Metrics
{startupTime && (
Startup: {startupTime.toFixed(2)}ms
)}
{currentScreenMetrics?.fps && (
FPS: {currentScreenMetrics.fps.toFixed(1)}
)}
{latestRequest && (
setIsNetworkExpanded(!isNetworkExpanded)}
>
Network Request
{isNetworkExpanded ? '▼' : '▶'}
→ {Math.round(latestRequest.duration).toFixed(1)}ms
{isNetworkExpanded && (
{latestRequest.status} {latestRequest.status >= 500 ? '🔴' : latestRequest.status >= 400 ? '🟠' : '🟢'}
{latestRequest.url}
)}
)}
{traces.length > 0 && (
setIsTracesExpanded(!isTracesExpanded)}
>
Recent Traces
{isTracesExpanded ? '▼' : '▶'}
{isTracesExpanded && traces.slice(-3).reverse().map((trace, idx) => (
{trace.interactionName} → {trace.componentName}
{trace.duration.toFixed(1)}ms
))}
)}
Copy Metrics
)}
Powered by Optic
>
)}
);
};
const styles = StyleSheet.create({
safeArea: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'box-none',
},
overlay: {
position: 'absolute',
backgroundColor: 'rgba(18, 18, 23, 0.98)',
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 16,
zIndex: 9999,
elevation: 20,
width: 320,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.4,
shadowRadius: 8,
},
collapsedOverlay: {
width: 'auto',
paddingVertical: 6,
paddingHorizontal: 12,
},
collapsedContainer: {
flexDirection: 'row',
alignItems: 'center',
},
collapsedMetrics: {
flexDirection: 'row',
gap: 16,
},
collapsedMetric: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
dragHandle: {
width: 40,
height: 4,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 4,
alignSelf: 'center',
marginBottom: 6,
},
header: {
marginBottom: 8,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.15)',
paddingBottom: 6,
},
headerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
headerButtons: {
flexDirection: 'row',
gap: 8,
},
iconButton: {
padding: 6,
borderRadius: 10,
backgroundColor: 'rgba(255, 255, 255, 0.15)',
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
},
icon: {
width: 18,
height: 18,
resizeMode: 'contain',
},
text: {
color: '#fff',
fontWeight: '700',
fontSize: 16,
letterSpacing: 0.3,
},
screenNameContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 6,
backgroundColor: 'rgba(255, 255, 255, 0.12)',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
},
screenName: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
fontStyle: 'italic',
},
content: {
marginTop: 6,
},
section: {
marginBottom: 12,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 12,
padding: 10,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
},
sectionTitle: {
color: '#fff',
fontSize: 14,
fontWeight: 'bold',
letterSpacing: 0.2,
},
metric: {
color: '#fff',
fontSize: 13,
marginBottom: 3,
fontWeight: '500',
},
traceDetails: {
marginTop: 4,
padding: 8,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
borderRadius: 8,
},
traceText: {
color: '#fff',
fontSize: 12,
marginBottom: 2,
},
copyButton: {
backgroundColor: 'rgba(33, 150, 243, 0.15)',
padding: 8,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
borderWidth: 1,
borderColor: 'rgba(33, 150, 243, 0.3)',
},
copyButtonText: {
color: '#2196F3',
fontSize: 12,
fontWeight: '600',
letterSpacing: 0.3,
},
poweredByContainer: {
alignSelf: 'flex-end',
marginTop: 6,
marginBottom: -2,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 4,
},
poweredByText: {
color: '#fff',
fontSize: 11,
fontWeight: '600',
opacity: 0.8,
letterSpacing: 0.3,
textDecorationLine: 'underline',
},
expandIcon: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
},
networkInfo: {
marginTop: 4,
},
expandedNetworkInfo: {
marginTop: 6,
padding: 8,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
borderRadius: 8,
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
statusCode: {
color: '#fff',
fontSize: 13,
fontWeight: 'bold',
},
urlContainer: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 8,
},
networkUrl: {
color: '#fff',
fontSize: 12,
marginBottom: 2,
},
traceRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 6,
},
traceScreen: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
},
traceDuration: {
color: '#fff',
fontSize: 12,
fontWeight: '500',
},
});
================================================
FILE: src/providers/OpticProvider.tsx
================================================
import React, { useEffect, useRef } from 'react';
import { useMetricsStore } from '../store/metricsStore';
import { Overlay } from '../overlay/Overlay';
import { useNavigation, useRoute, useNavigationContainerRef } from '@react-navigation/native';
import { usePathname, useSegments } from 'expo-router';
import { initRenderTracking } from '../metrics/globalRenderTracking';
import { FPSManager } from '../metrics/fps';
interface OpticProviderProps {
children: React.ReactNode;
/**
* Enable or disable specific metrics
*/
metrics?: {
enabled?: boolean;
startup?: boolean;
reRenders?: boolean;
fps?: boolean;
network?: boolean;
traces?: boolean;
};
/**
* Show or hide the performance overlay
*/
showOverlay?: boolean;
}
const defaultMetrics = {
enabled: true,
startup: true,
reRenders: true,
fps: true,
network: true,
traces: true,
};
export const OpticProvider: React.FC = ({
children,
metrics = defaultMetrics,
showOverlay = true
}) => {
const { setCurrentScreen } = useMetricsStore();
const currentScreen = useMetricsStore((state) => state.currentScreen);
const pathname = usePathname();
const segments = useSegments();
const navigationRef = useNavigationContainerRef();
const fpsManager = React.useRef(null);
// Navigation hooks
const navigation = useNavigation();
const route = useRoute();
// Initialize re-render tracking if enabled
useEffect(() => {
if (metrics.reRenders) {
initRenderTracking();
}
}, [metrics.reRenders]);
useEffect(() => {
if (metrics.enabled && metrics.fps) {
fpsManager.current = new FPSManager();
fpsManager.current.startTracking();
}
return () => {
if (fpsManager.current) {
fpsManager.current.stopTracking();
}
};
}, [metrics.enabled, metrics.fps]);
// Function to get the current screen name
const getCurrentScreenName = () => {
// Try to get screen name from Expo Router first
if (pathname) {
return pathname;
}
// Fallback to React Navigation
if (navigationRef.current) {
const currentRoute = navigationRef.current.getCurrentRoute();
if (currentRoute?.name) {
return currentRoute.name;
}
}
// If no screen name is found, use the first segment or default to 'index'
return segments[0] || 'index';
};
// Handle screen changes and initial route
useEffect(() => {
const screenName = getCurrentScreenName();
// Always set the current screen, even if it's the same
// This ensures we capture the initial route
setCurrentScreen(screenName);
}, [pathname, segments, navigationRef.current]);
return (
<>
{children}
{showOverlay && }
>
);
};
================================================
FILE: src/store/metricsStore.ts
================================================
import { create } from 'zustand';
import { InitOpticOptions } from '../core/initOptic';
export interface NetworkRequest {
url: string;
method: string;
duration: number;
status: number;
[key: string]: any; // for any extra fields
}
export interface Trace {
interactionName: string;
componentName: string;
duration: number;
timestamp: number;
}
export interface MetricsState {
currentScreen: string | null;
screens: Record;
fps: number | null;
}>;
networkRequests: NetworkRequest[];
traces: Trace[];
startupTime: number | null;
setCurrentScreen: (screenName: string | null) => void;
incrementReRender: (componentName: string) => void;
setStartupTime: (time: number) => void;
setFPS: (fps: number, screenName: string) => void;
addNetworkRequest: (request: NetworkRequest) => void;
setTrace: (trace: Trace) => void;
}
export const useMetricsStore = create((set, get) => ({
currentScreen: null,
screens: {},
networkRequests: [],
traces: [],
startupTime: null,
setCurrentScreen: (screenName) => {
set((state) => {
// Initialize screen metrics if they don't exist
if (screenName && !state.screens[screenName]) {
return {
currentScreen: screenName,
screens: {
...state.screens,
[screenName]: {
reRenderCounts: {},
fps: null,
},
},
};
}
return { currentScreen: screenName };
});
},
incrementReRender: (componentName) => {
const state = get();
if (!state.currentScreen) return;
const currentScreen = state.screens[state.currentScreen];
const currentCount = currentScreen.reRenderCounts[componentName] || 0;
set((state) => ({
screens: {
...state.screens,
[state.currentScreen!]: {
...currentScreen,
reRenderCounts: {
...currentScreen.reRenderCounts,
[componentName]: currentCount + 1,
},
},
},
}));
},
setStartupTime: (time) => {
set({ startupTime: time });
},
setFPS: (fps, screenName) => {
set((state) => ({
screens: {
...state.screens,
[screenName]: {
...state.screens[screenName],
fps,
},
},
}));
},
addNetworkRequest: (request) => {
set((state) => ({
networkRequests: [...state.networkRequests, request].slice(-50), // Keep last 50 requests
}));
},
setTrace: (trace) => {
set((state) => ({
traces: [...state.traces, trace].slice(-10), // Keep last 10 traces
}));
},
}));
export let opticEnabled = true;
export function setOpticEnabled(value: boolean) {
opticEnabled = value;
}
export function initOptic(options: InitOpticOptions = {}) {
const { enabled = true, onMetricsLogged } = options;
opticEnabled = enabled;
if (!enabled) {
return;
}
// ...rest of your logic...
}
================================================
FILE: src/types/global.d.ts
================================================
declare module '*.png' {
const value: any;
export default value;
}
================================================
FILE: src/utils/logger.ts
================================================
const isDevelopment = process.env.NODE_ENV === 'development';
export const logger = {
debug: (...args: any[]) => {
if (isDevelopment) {
console.log('[useoptic]', ...args);
}
},
warn: (...args: any[]) => {
if (isDevelopment) {
console.warn('[useoptic]', ...args);
}
},
error: (...args: any[]) => {
console.error('[useoptic]', ...args);
}
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2017",
"module": "ESNext",
"lib": ["ES2017", "DOM"],
"jsx": "react",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
================================================
FILE: tsup.config.ts
================================================
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
dts: true,
format: ['esm', 'cjs'],
outDir: 'dist',
sourcemap: true,
clean: true,
external: ['react', 'react-native'],
esbuildOptions(options) {
options.jsx = 'automatic';
},
});