Repository: adnxy/optic-react-native
Branch: main
Commit: e445c8f77b4c
Files: 32
Total size: 60.8 KB
Directory structure:
gitextract_9wjfwnn8/
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── App.tsx
├── CONTRIBUTING.md
├── README.md
├── app.json
├── babel.config.js
├── metro.config.js
├── package.json
├── src/
│ ├── components/
│ │ ├── OpticProvider.tsx
│ │ └── PerformanceOverlay.tsx
│ ├── core/
│ │ └── initOptic.ts
│ ├── hoc/
│ │ └── withScreenTracking.tsx
│ ├── hooks/
│ │ ├── useAutoScreenName.ts
│ │ └── useScreenName.ts
│ ├── index.ts
│ ├── index.tsx
│ ├── metrics/
│ │ ├── fps.ts
│ │ ├── globalRenderTracking.ts
│ │ ├── network.ts
│ │ ├── reRenders.ts
│ │ ├── screen.ts
│ │ ├── startup.ts
│ │ └── trace.ts
│ ├── overlay/
│ │ └── Overlay.tsx
│ ├── providers/
│ │ └── OpticProvider.tsx
│ ├── store/
│ │ └── metricsStore.ts
│ ├── types/
│ │ └── global.d.ts
│ └── utils/
│ └── logger.ts
├── tsconfig.json
└── tsup.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }]
},
"settings": {
"react": {
"version": "detect"
}
}
}
================================================
FILE: .gitignore
================================================
# OSX
.DS_Store
Thumbs.db
# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
ios/.xcode.env.local
# Android/IntelliJ
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore
# node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# fastlane
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
# Bundle artifact
*.jsbundle
# Ruby / CocoaPods
/ios/Pods/
/vendor/bundle/
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
# testing
/coverage
# Production build
/dist
/build
# TypeScript
*.tsbuildinfo
# Environment variables
.env
.env.*
!.env.example
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
logs
*.log
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# Expo
.expo/
web-build/
dist/
# Dependencies
.pnp/
.pnp.js
================================================
FILE: .npmignore
================================================
# Source
src/
tests/
__tests__/
*.test.ts
*.test.tsx
*.spec.ts
*.spec.tsx
# Development
.git/
.github/
.gitignore
.eslintrc.json
.prettierrc
jest.config.js
tsconfig.json
tsup.config.ts
*.config.js
*.config.ts
# IDE
.idea/
.vscode/
*.swp
*.swo
*.sublime-*
# OS
.DS_Store
Thumbs.db
desktop.ini
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Environment
.env
.env.*
!.env.example
# Build
coverage/
.nyc_output/
dist/
build/
*.tgz
# Dependencies
node_modules/
.pnp/
.pnp.js
# TypeScript
*.tsbuildinfo
================================================
FILE: App.tsx
================================================
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { initOptic } from './src';
import { Overlay } from './src/overlay/Overlay';
import { RenderTest } from './src/components/RenderTest';
import { OpticProvider } from './src/providers/OpticProvider';
import { initRenderTracking } from './src/metrics/globalRenderTracking';
// Initialize Optic with all metrics enabled
initOptic({
enabled: true,
network: true,
startup: true,
reRenders: true,
traces: true
});
// Initialize re-render tracking
initRenderTracking();
export default function App() {
return (
<OpticProvider>
<View style={styles.container}>
<RenderTest />
<Overlay />
</View>
</OpticProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to optic-react-native
We love your input! We want to make contributing to optic-react-native as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
## We Develop with Github
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html)
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issue tracker](https://github.comoptic-react-native/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.comoptic-react-native/issues/new); it's that easy!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
## Development Process
1. Clone the repository
2. Install dependencies:
```bash
npm install
```
3. Make your changes
4. Build the project:
```bash
npm run build
```
5. Test your changes
6. Submit a pull request
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
================================================
FILE: README.md
================================================
# optic-react-native
A lightweight performance monitoring tool for React Native applications. Track startup time, network requests, FPS, and custom traces in real-time.

## Features
- App startup time measurement
- Network request tracking
- Custom interaction tracing
- Draggable overlay display
- Send metrics to custom API
## Demo
<img src="https://github.com/user-attachments/assets/d7cc525f-5621-4107-9cce-ef3f8a0dac0f" width="350" alt="Optic Performance Monitor Screenshot" />
## Installation
```bash
npm install optic-react-native
# or
yarn add optic-react-native
```
## Quick Start
1. Initialize Optic in your app's entry point:
```typescript
import { initOptic } from 'optic-react-native';
initOptic();
```
2. Add the overlay component to your app:
```typescript
import { OpticProvider } from 'optic-react-native';
const App = () => (
<>
<OpticProvider>
<YourAppContent />
<OpticProvider />
</>
);
```
## Custom Metrics
### Tracking Custom Interactions
Use the tracing API to measure specific interactions in your app:
```typescript
import { startTrace, endTrace } from 'optic-react-native';
const handleButtonPress = async () => {
startTrace('ButtonPress');
try {
await someAsyncOperation();
} finally {
endTrace('ButtonPress', 'ButtonComponent');
}
};
```
### Tracking Re-renders
Monitor component re-renders using the `useRenderMonitor` hook:
```typescript
import { useRenderMonitor } from 'optic-react-native;
const MyComponent = () => {
useRenderMonitor('MyComponent');
// ... your component code
};
```
## Configuration
```typescript
interface InitOpticOptions {
enabled?: boolean; // Enable/disable all metrics (default: true)
startup?: boolean; // Track startup time (default: true)
network?: boolean; // Track network requests (default: true)
reRenders?: boolean; // Track component re-renders (default: true)
traces?: boolean; // Enable custom tracing (default: true)
onMetricsLogged?: (metrics: any) => void; // Callback for metrics updates
}
```
## Contributing
We welcome contributions! Here's how you can help:
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Development Setup
1. Clone the repository
2. Install dependencies: `yarn install`
3. Run tests: `yarn test`
4. Build the package: `yarn build`
### Code Style
- Follow the existing code style
- Write tests for new features
- Update documentation as needed
- Keep commits atomic and well-described
## License
MIT © Optic
Copyright (c) 2024 Optic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Optic React Native
### Initialization Options
#### `enabled`
- **Type:** `boolean`
- **Default:** `true`
- **Description:** If set to `false`, disables all metrics and hides the overlay. Useful for turning off performance tracking in production or for specific builds.
#### `onMetricsLogged`
- **Type:** `(metrics: any) => void`
- **Description:** Callback function that is called whenever metrics are updated. You can use this to log metrics, send them to an API, or perform custom analytics.
#### Example Usage
```js
import { initOptic } from 'optic-react-native';
initOptic({
enabled: true, // Set to false to disable metrics and overlay
onMetricsLogged: (metrics) => {
// Log metrics or send to your API
fetch('https://your-api.com/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metrics),
});
},
// ...other options
});
```
---
For more details on all options, see the API documentation section.
## Testing Re-render Tracking with `useRenderMonitor`
To test and visualize component re-renders in the overlay, use the `useRenderMonitor` hook in your components. This will increment the re-render count for the component in the metrics overlay.
### Example Usage
```tsx
import React from 'react';
import { useRenderMonitor } from 'optic-react-native';
export const TestComponent = () => {
useRenderMonitor('Home');
const [count, setCount] = React.useState(0);
return (
<View>
<Text>Render count: {count}</Text>
<Button title="Re-render" onPress={() => setCount(count + 1)} />
</View>
);
};
```
- The name you pass to `useRenderMonitor` will appear in the "Re-renders" section of the overlay.
- Each time the component re-renders, the count will increment in real time.
Add this component to your app and interact with it to see re-render tracking in action in the overlay.
---
================================================
FILE: app.json
================================================
{
"expo": {
"name": "optic-react-native",
"slug": "useoptic-react-native",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.useoptic.app"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.useoptic.app"
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-device"
]
}
}
================================================
FILE: babel.config.js
================================================
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
// Add any other plugins you need
],
};
};
================================================
FILE: metro.config.js
================================================
const { getDefaultConfig } = require('@react-native/metro-config');
module.exports = (async () => {
const defaultConfig = await getDefaultConfig(__dirname);
const { assetExts } = defaultConfig.resolver;
return {
...defaultConfig,
resolver: {
...defaultConfig.resolver,
assetExts: [...assetExts, 'png'],
},
};
})();
================================================
FILE: package.json
================================================
{
"name": "optic-react-native",
"author": "Adnan Sahinovic",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsup src/index.ts --dts --format esm,cjs --out-dir dist",
"dev": "tsup --watch",
"prepare": "tsup",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["react-native", "react", "optic", "metrics", "performance", "debugging"],
"license": "MIT",
"description": "React Native library for Optic",
"dependencies": {
"@react-navigation/native": "^7.1.9",
"expo-router": "^5.0.7",
"react": "*",
"react-native": "*",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/react": "^19.1.3",
"@types/react-native": "^0.72.8",
"react-native-safe-area-context": "^4.9.0",
"tsup": "^8.4.0",
"typescript": "^5.8.3"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">=4.0.0"
}
}
================================================
FILE: src/components/OpticProvider.tsx
================================================
import React from 'react';
import { View } from 'react-native';
import { useMetricsStore } from '../store/metricsStore';
import { Overlay } from '../overlay/Overlay';
// Define the types we need since we don't want to add @react-navigation/native as a dependency
interface NavigationState {
routes: Array<{
name: string;
[key: string]: any;
}>;
index: number;
}
interface NavigationContainerProps {
onStateChange?: (state: NavigationState | undefined) => void;
[key: string]: any;
}
interface OpticProviderProps {
children: React.ReactNode;
}
export function OpticProvider({ children }: OpticProviderProps) {
const setCurrentScreen = useMetricsStore((state) => state.setCurrentScreen);
// Function to handle navigation state changes
const handleNavigationStateChange = (state: NavigationState | undefined) => {
if (state?.routes && state.routes.length > 0) {
const currentRoute = state.routes[state.index];
if (currentRoute?.name) {
setCurrentScreen(currentRoute.name);
}
}
};
// Clone children and add onStateChange prop to NavigationContainer
const childrenWithNavigationTracking = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
// Check if the child is a NavigationContainer by checking its displayName
const displayName = (child.type as any)?.displayName || (child.type as any)?.name;
if (displayName === 'NavigationContainer') {
return React.cloneElement(child, {
onStateChange: handleNavigationStateChange,
} as Partial<NavigationContainerProps>);
}
}
return child;
});
return (
<View style={{ flex: 1 }}>
{childrenWithNavigationTracking}
<Overlay />
</View>
);
}
================================================
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 (
<View style={styles.container}>
<Text style={styles.title}>Performance Metrics</Text>
<Text style={styles.metric}>FPS: {fps ? `${fps.toFixed(1)}` : 'N/A'}</Text>
<Text style={styles.metric}>Screen: {currentScreen || 'N/A'}</Text>
<Text style={styles.metric}>Traces: {traces.length}</Text>
</View>
);
}
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<P extends object>(WrappedComponent: React.ComponentType<P>) {
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<P extends object>(Component: React.ComponentType<P>): React.ComponentType<P> {
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<P extends object>(
WrappedComponent: React.ComponentType<P>,
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 <WrappedComponent {...props} />;
};
}
================================================
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 <View>...</View>;
* }
*/
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 <View>...</View>;
* }
*/
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<any> | undefined;
var __OPTIC_RENDER_TRACKING_ENABLED__: boolean;
}
// Store to keep track of component renders
const renderCounts: Record<string, number> = {};
// Create a wrapper component that tracks renders
const withRenderTracking = (WrappedComponent: React.ComponentType<any>) => {
const RenderTrackingWrapper: React.FC<any> = (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<T extends React.ComponentType<any>>(
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<any>) {
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<string, { startTime: number; url: string; method: string }>();
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<string, { from: any; to: any }>;
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<T extends Record<string, any>>(
componentName: string,
props: T,
options: {
debug?: boolean;
ignoreProps?: string[];
trackStack?: boolean;
} = {}
) {
if (!React) return;
const { ignoreProps = [], trackStack = false } = options;
const prevProps = useRef<T | null>(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<string, { from: any; to: any }> = {};
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<string | null>(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<string, number> = 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 = () => (
<View style={styles.collapsedContainer}>
<View style={styles.collapsedMetrics}>
<Text style={styles.collapsedMetric}>
🚀 {startupTime !== null ? `${startupTime.toFixed(1)}ms` : '...'}
</Text>
<Text style={styles.collapsedMetric}>
🎮 {currentScreenMetrics?.fps !== null && currentScreenMetrics?.fps !== undefined ? `${currentScreenMetrics.fps.toFixed(1)}` : '...'}
</Text>
</View>
</View>
);
if (!currentScreen) return null;
return (
<SafeAreaView style={styles.safeArea} pointerEvents="box-none">
<Animated.View
style={[
styles.overlay,
isCollapsed ? styles.collapsedOverlay : null,
{
left: position.x,
top: position.y,
},
]}
{...panResponder.panHandlers}
>
<TouchableOpacity
style={styles.dragHandle}
onPress={() => setIsCollapsed(!isCollapsed)}
/>
{isCollapsed ? (
renderCollapsedView()
) : (
<>
<View style={styles.header}>
<View style={styles.headerTop}>
<Text style={styles.text}>Performance Metrics</Text>
<View style={styles.headerButtons}>
<TouchableOpacity
style={[styles.iconButton]}
onPress={() => setIsMinimized(!isMinimized)}
>
<Image
source={{ uri: isMinimized ? maximizeImageUrl : minimizeImageUrl }}
style={styles.icon}
/>
</TouchableOpacity>
</View>
</View>
<View style={styles.screenNameContainer}>
<Text style={styles.screenName}>
{currentScreen || 'No Screen'}
</Text>
</View>
</View>
{!isMinimized && (
<ScrollView style={styles.content}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Performance Metrics</Text>
{startupTime && (
<Text style={[styles.metric, { color: getMetricColor('STARTUP', startupTime) }]}>Startup: {startupTime.toFixed(2)}ms</Text>
)}
{currentScreenMetrics?.fps && (
<Text style={[styles.metric, { color: getMetricColor('FPS', currentScreenMetrics.fps) }]}>FPS: {currentScreenMetrics.fps.toFixed(1)}</Text>
)}
</View>
{latestRequest && (
<View style={styles.section}>
<TouchableOpacity
style={styles.sectionHeader}
onPress={() => setIsNetworkExpanded(!isNetworkExpanded)}
>
<Text style={styles.sectionTitle}>Network Request</Text>
<Text style={styles.expandIcon}>{isNetworkExpanded ? '▼' : '▶'}</Text>
</TouchableOpacity>
<View style={styles.networkInfo}>
<Text style={[styles.metric, { color: getNetworkColor(latestRequest.duration) }]}>→ {Math.round(latestRequest.duration).toFixed(1)}ms</Text>
{isNetworkExpanded && (
<View style={styles.expandedNetworkInfo}>
<View style={styles.statusContainer}>
<Text style={[styles.statusCode, { color: getStatusColor(latestRequest.status) }]}>
{latestRequest.status} {latestRequest.status >= 500 ? '🔴' : latestRequest.status >= 400 ? '🟠' : '🟢'}
</Text>
</View>
<View style={styles.urlContainer}>
<Text style={styles.networkUrl} numberOfLines={1} ellipsizeMode="middle">{latestRequest.url}</Text>
</View>
</View>
)}
</View>
</View>
)}
{traces.length > 0 && (
<View style={styles.section}>
<TouchableOpacity
style={styles.sectionHeader}
onPress={() => setIsTracesExpanded(!isTracesExpanded)}
>
<Text style={styles.sectionTitle}>Recent Traces</Text>
<Text style={styles.expandIcon}>{isTracesExpanded ? '▼' : '▶'}</Text>
</TouchableOpacity>
{isTracesExpanded && traces.slice(-3).reverse().map((trace, idx) => (
<View key={idx} style={styles.traceRow}>
<Text style={styles.traceScreen}>{trace.interactionName} → {trace.componentName}</Text>
<Text style={[styles.traceDuration, { color: getMetricColor('TRACE', trace.duration) }]}>{trace.duration.toFixed(1)}ms</Text>
</View>
))}
</View>
)}
<TouchableOpacity style={styles.copyButton} onPress={handleCopyMetrics}>
<Text style={styles.copyButtonText}>Copy Metrics</Text>
</TouchableOpacity>
</ScrollView>
)}
<View style={styles.poweredByContainer}>
<TouchableOpacity onPress={handleOpenWebsite}>
<Text style={styles.poweredByText}>Powered by Optic</Text>
</TouchableOpacity>
</View>
</>
)}
</Animated.View>
</SafeAreaView>
);
};
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<OpticProviderProps> = ({
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<FPSManager | null>(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 && <Overlay />}
</>
);
};
================================================
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<string, {
reRenderCounts: Record<string, number>;
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<MetricsState>((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';
},
});
gitextract_9wjfwnn8/ ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── App.tsx ├── CONTRIBUTING.md ├── README.md ├── app.json ├── babel.config.js ├── metro.config.js ├── package.json ├── src/ │ ├── components/ │ │ ├── OpticProvider.tsx │ │ └── PerformanceOverlay.tsx │ ├── core/ │ │ └── initOptic.ts │ ├── hoc/ │ │ └── withScreenTracking.tsx │ ├── hooks/ │ │ ├── useAutoScreenName.ts │ │ └── useScreenName.ts │ ├── index.ts │ ├── index.tsx │ ├── metrics/ │ │ ├── fps.ts │ │ ├── globalRenderTracking.ts │ │ ├── network.ts │ │ ├── reRenders.ts │ │ ├── screen.ts │ │ ├── startup.ts │ │ └── trace.ts │ ├── overlay/ │ │ └── Overlay.tsx │ ├── providers/ │ │ └── OpticProvider.tsx │ ├── store/ │ │ └── metricsStore.ts │ ├── types/ │ │ └── global.d.ts │ └── utils/ │ └── logger.ts ├── tsconfig.json └── tsup.config.ts
SYMBOL INDEX (43 symbols across 18 files)
FILE: App.tsx
function App (line 21) | function App() {
FILE: src/components/OpticProvider.tsx
type NavigationState (line 7) | interface NavigationState {
type NavigationContainerProps (line 15) | interface NavigationContainerProps {
type OpticProviderProps (line 20) | interface OpticProviderProps {
function OpticProvider (line 24) | function OpticProvider({ children }: OpticProviderProps) {
FILE: src/components/PerformanceOverlay.tsx
function PerformanceOverlay (line 5) | function PerformanceOverlay() {
FILE: src/core/initOptic.ts
type InitOpticOptions (line 8) | interface InitOpticOptions {
type OpticConfig (line 17) | interface OpticConfig {
function withScreenTracking (line 27) | function withScreenTracking<P extends object>(WrappedComponent: React.Co...
function isScreenComponent (line 47) | function isScreenComponent(component: any): boolean {
function wrapIfScreen (line 56) | function wrapIfScreen<P extends object>(Component: React.ComponentType<P...
function initOptic (line 72) | function initOptic(options: InitOpticOptions = {}) {
FILE: src/hoc/withScreenTracking.tsx
function withScreenTracking (line 5) | function withScreenTracking<P extends object>(
FILE: src/hooks/useAutoScreenName.ts
function useAutoScreenName (line 14) | function useAutoScreenName() {
FILE: src/hooks/useScreenName.ts
function useScreenName (line 14) | function useScreenName(screenName: string) {
FILE: src/metrics/fps.ts
type FPSMetrics (line 3) | interface FPSMetrics {
class FPSManager (line 8) | class FPSManager {
method constructor (line 14) | constructor() {
FILE: src/metrics/globalRenderTracking.ts
function wrapWithRenderTracking (line 38) | function wrapWithRenderTracking<T extends React.ComponentType<any>>(
function setRenderTrackingEnabled (line 52) | function setRenderTrackingEnabled(enabled: boolean) {
function setupGlobalRenderTracking (line 57) | function setupGlobalRenderTracking() {
function setRootComponent (line 70) | function setRootComponent(component: React.ComponentType<any>) {
function initRenderTracking (line 82) | function initRenderTracking() {
FILE: src/metrics/network.ts
constant NETWORK_THRESHOLDS (line 4) | const NETWORK_THRESHOLDS = {
FILE: src/metrics/reRenders.ts
type ReRenderInfo (line 4) | interface ReRenderInfo {
function useRenderMonitor (line 18) | function useRenderMonitor<T extends Record<string, any>>(
function setupRenderTracking (line 79) | function setupRenderTracking(options: {
FILE: src/metrics/screen.ts
function useScreenMetrics (line 8) | function useScreenMetrics(screenName: string) {
FILE: src/metrics/startup.ts
function trackStartupTime (line 23) | function trackStartupTime() {
FILE: src/metrics/trace.ts
type Trace (line 3) | interface Trace {
class TraceManager (line 10) | class TraceManager {
method startTrace (line 19) | startTrace(interactionName: string) {
method endTrace (line 29) | endTrace(interactionName: string, componentName: string) {
method getTraces (line 55) | getTraces(): Trace[] {
method clearTraces (line 62) | clearTraces() {
FILE: src/overlay/Overlay.tsx
constant METRICS_THRESHOLDS (line 14) | const METRICS_THRESHOLDS = {
FILE: src/providers/OpticProvider.tsx
type OpticProviderProps (line 9) | interface OpticProviderProps {
FILE: src/store/metricsStore.ts
type NetworkRequest (line 4) | interface NetworkRequest {
type Trace (line 12) | interface Trace {
type MetricsState (line 19) | interface MetricsState {
function setOpticEnabled (line 114) | function setOpticEnabled(value: boolean) {
function initOptic (line 118) | function initOptic(options: InitOpticOptions = {}) {
FILE: tsup.config.ts
method esbuildOptions (line 11) | esbuildOptions(options) {
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (66K chars).
[
{
"path": ".eslintrc.json",
"chars": 779,
"preview": "{\n \"env\": {\n \"browser\": true,\n \"es2021\": true,\n \"node\": true\n },\n \"extends\": [\n \"eslint:recommended\",\n "
},
{
"path": ".gitignore",
"chars": 1270,
"preview": "# OSX\n.DS_Store\nThumbs.db\n\n# Xcode\nbuild/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2"
},
{
"path": ".npmignore",
"chars": 543,
"preview": "# Source\nsrc/\ntests/\n__tests__/\n*.test.ts\n*.test.tsx\n*.spec.ts\n*.spec.tsx\n\n# Development\n.git/\n.github/\n.gitignore\n.esli"
},
{
"path": "App.tsx",
"chars": 846,
"preview": "import React from 'react';\nimport { View, StyleSheet } from 'react-native';\nimport { initOptic } from './src';\nimport { "
},
{
"path": "CONTRIBUTING.md",
"chars": 2103,
"preview": "# Contributing to optic-react-native\n\nWe love your input! We want to make contributing to optic-react-native as easy and"
},
{
"path": "README.md",
"chars": 5746,
"preview": "# optic-react-native\n\nA lightweight performance monitoring tool for React Native applications. Track startup time, netwo"
},
{
"path": "app.json",
"chars": 775,
"preview": "{\n \"expo\": {\n \"name\": \"optic-react-native\",\n \"slug\": \"useoptic-react-native\",\n \"version\": \"1.0.0\",\n \"orient"
},
{
"path": "babel.config.js",
"chars": 170,
"preview": "module.exports = function (api) {\n api.cache(true);\n return {\n presets: ['babel-preset-expo'],\n plugins: [\n "
},
{
"path": "metro.config.js",
"chars": 349,
"preview": "const { getDefaultConfig } = require('@react-native/metro-config');\n\nmodule.exports = (async () => {\n const defaultConf"
},
{
"path": "package.json",
"chars": 1011,
"preview": "{\n \"name\": \"optic-react-native\",\n \"author\": \"Adnan Sahinovic\",\n \"version\": \"1.0.0\",\n \"main\": \"dist/index.js\",\n \"typ"
},
{
"path": "src/components/OpticProvider.tsx",
"chars": 1761,
"preview": "import React from 'react';\nimport { View } from 'react-native';\nimport { useMetricsStore } from '../store/metricsStore';"
},
{
"path": "src/components/PerformanceOverlay.tsx",
"chars": 953,
"preview": "import React from 'react';\nimport { View, Text, StyleSheet } from 'react-native';\nimport { useMetricsStore } from '../st"
},
{
"path": "src/core/initOptic.ts",
"chars": 3348,
"preview": "import { initRenderTracking } from '../metrics/globalRenderTracking';\nimport { initNetworkTracking } from '../metrics/ne"
},
{
"path": "src/hoc/withScreenTracking.tsx",
"chars": 956,
"preview": "import React, { useEffect } from 'react';\nimport { useNavigation } from '@react-navigation/native';\nimport { useMetricsS"
},
{
"path": "src/hooks/useAutoScreenName.ts",
"chars": 976,
"preview": "import { useEffect } from 'react';\nimport { useMetricsStore } from '../store/metricsStore';\n\n/**\n * Automatically tracks"
},
{
"path": "src/hooks/useScreenName.ts",
"chars": 588,
"preview": "import { useEffect } from 'react';\nimport { useMetricsStore } from '../store/metricsStore';\n\n/**\n * A simple hook to tra"
},
{
"path": "src/index.ts",
"chars": 331,
"preview": "export { initOptic } from './core/initOptic';\nexport { OpticProvider } from './providers/OpticProvider';\nexport { useMet"
},
{
"path": "src/index.tsx",
"chars": 353,
"preview": "import { OpticProvider } from './components/OpticProvider';\nimport { initOptic } from './core/initOptic';\nimport { Overl"
},
{
"path": "src/metrics/fps.ts",
"chars": 1581,
"preview": "import { useMetricsStore } from '../store/metricsStore';\n\nexport interface FPSMetrics {\n fps: number;\n timestamp: numb"
},
{
"path": "src/metrics/globalRenderTracking.ts",
"chars": 2764,
"preview": "import * as React from 'react';\nimport { useMetricsStore } from '../store/metricsStore';\n\ndeclare global {\n var __OPTIC"
},
{
"path": "src/metrics/network.ts",
"chars": 6787,
"preview": "import { useMetricsStore } from '../store/metricsStore';\n\n// Network performance thresholds (in milliseconds)\nconst NETW"
},
{
"path": "src/metrics/reRenders.ts",
"chars": 2272,
"preview": "import React, { useEffect, useRef } from 'react';\nimport { useMetricsStore } from '../store/metricsStore';\n\ninterface Re"
},
{
"path": "src/metrics/screen.ts",
"chars": 1051,
"preview": "import { useEffect, useRef, useCallback } from 'react';\nimport { useMetricsStore } from '../store/metricsStore';\n\n/**\n *"
},
{
"path": "src/metrics/startup.ts",
"chars": 1202,
"preview": "export {};\n\nimport { useMetricsStore } from '../store/metricsStore';\n\n// Global app start time (should be set as early a"
},
{
"path": "src/metrics/trace.ts",
"chars": 1703,
"preview": "import { useMetricsStore } from '../store/metricsStore';\n\ninterface Trace {\n interactionName: string;\n componentName: "
},
{
"path": "src/overlay/Overlay.tsx",
"chars": 14906,
"preview": "import React, { useRef, useState } from 'react';\nimport { View, Text, StyleSheet, PanResponder, Animated, Dimensions, To"
},
{
"path": "src/providers/OpticProvider.tsx",
"chars": 2806,
"preview": "import React, { useEffect, useRef } from 'react';\nimport { useMetricsStore } from '../store/metricsStore';\nimport { Over"
},
{
"path": "src/store/metricsStore.ts",
"chars": 3002,
"preview": "import { create } from 'zustand';\nimport { InitOpticOptions } from '../core/initOptic';\n\nexport interface NetworkRequest"
},
{
"path": "src/types/global.d.ts",
"chars": 71,
"preview": "declare module '*.png' {\n const value: any;\n export default value;\n} "
},
{
"path": "src/utils/logger.ts",
"chars": 387,
"preview": "const isDevelopment = process.env.NODE_ENV === 'development';\n\nexport const logger = {\n debug: (...args: any[]) => {\n "
},
{
"path": "tsconfig.json",
"chars": 543,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"module\": \"ESNext\",\n \"lib\": [\"ES2017\", \"DOM\"],\n \"js"
},
{
"path": "tsup.config.ts",
"chars": 292,
"preview": "import { defineConfig } from 'tsup';\n\nexport default defineConfig({\n entry: ['src/index.ts'],\n dts: true,\n format: ['"
}
]
About this extraction
This page contains the full source code of the adnxy/optic-react-native GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (60.8 KB), approximately 15.8k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.