react-native-inapp-inspector 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/package.json +2 -1
- package/src/analytics.ts +35 -0
- package/src/bundle.ts +25 -0
- package/src/components/AnalyticsDetail.tsx +865 -0
- package/src/components/AnalyticsEventCard.tsx +441 -0
- package/src/components/AnalyticsGraph.tsx +583 -0
- package/src/components/AnimatedEntrance.tsx +64 -0
- package/src/components/AppHeaderLogo.tsx +109 -0
- package/src/components/BrandCircleIcon.tsx +144 -0
- package/src/components/BrandSquareIcon.tsx +144 -0
- package/src/components/CodeSnippet.tsx +725 -0
- package/src/components/ConsoleLogCard.tsx +628 -0
- package/src/components/CopyButton.tsx +87 -0
- package/src/components/DiffViewer.tsx +82 -0
- package/src/components/DomainHeader.tsx +237 -0
- package/src/components/EmptyState.tsx +73 -0
- package/src/components/EndOfListFooter.tsx +100 -0
- package/src/components/ErrorBoundary.tsx +688 -0
- package/src/components/HeadersSection.tsx +192 -0
- package/src/components/HighlightText.tsx +100 -0
- package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
- package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
- package/src/components/Inspector/BundleTab.tsx +4731 -0
- package/src/components/Inspector/ConsoleTab.tsx +702 -0
- package/src/components/Inspector/CrashDetail.tsx +941 -0
- package/src/components/Inspector/CrashFilterModal.tsx +721 -0
- package/src/components/Inspector/CrashTab.tsx +871 -0
- package/src/components/Inspector/FabLauncher.tsx +80 -0
- package/src/components/Inspector/InspectorContext.tsx +28 -0
- package/src/components/Inspector/InspectorHeader.tsx +973 -0
- package/src/components/Inspector/LogDetail.tsx +1427 -0
- package/src/components/Inspector/MainScreen.tsx +258 -0
- package/src/components/Inspector/NavigationTracker.tsx +13 -0
- package/src/components/Inspector/NetworkDetail.tsx +794 -0
- package/src/components/Inspector/NetworkTab.tsx +1095 -0
- package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
- package/src/components/Inspector/PerformanceTab.tsx +1894 -0
- package/src/components/Inspector/ReduxDetail.tsx +1651 -0
- package/src/components/Inspector/ReduxTab.tsx +954 -0
- package/src/components/Inspector/SettingsPanel.tsx +3181 -0
- package/src/components/Inspector/TabBar.tsx +187 -0
- package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
- package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
- package/src/components/JsonViewer.tsx +486 -0
- package/src/components/LogCard.tsx +491 -0
- package/src/components/LogSyntaxHighlighter.tsx +178 -0
- package/src/components/MetaAccordion.tsx +340 -0
- package/src/components/MiniBarChart.tsx +42 -0
- package/src/components/MiniLineChart.tsx +33 -0
- package/src/components/NetworkIcons.tsx +2118 -0
- package/src/components/SectionHeader.tsx +113 -0
- package/src/components/SegmentedTabs.tsx +83 -0
- package/src/components/Slider.tsx +300 -0
- package/src/components/SourcePageCard.tsx +148 -0
- package/src/components/Toast.tsx +131 -0
- package/src/components/TouchableScale.tsx +91 -0
- package/src/components/TreeNode.tsx +186 -0
- package/src/console.ts +24 -0
- package/src/constants/index.ts +38 -0
- package/src/constants/version.ts +3 -0
- package/src/crash.ts +32 -0
- package/src/customHooks/analyticsLogger.ts +336 -0
- package/src/customHooks/bundleAnalyzer.ts +1231 -0
- package/src/customHooks/consoleLogger.ts +497 -0
- package/src/customHooks/crashHandler.ts +944 -0
- package/src/customHooks/logFilters.ts +32 -0
- package/src/customHooks/networkLogger.ts +419 -0
- package/src/customHooks/performanceTracker.ts +1014 -0
- package/src/customHooks/reduxLogger.ts +406 -0
- package/src/customHooks/useAccordion.tsx +60 -0
- package/src/decorators/index.ts +184 -0
- package/src/helpers/gaAnalyticsRegistry.ts +204 -0
- package/src/helpers/index.ts +860 -0
- package/src/helpers/memoryManager.ts +138 -0
- package/src/helpers/searchQueryParser.ts +283 -0
- package/src/helpers/settingsStore.ts +171 -0
- package/src/helpers/telemetry.ts +505 -0
- package/src/helpers/toast.ts +17 -0
- package/src/i18n/index.ts +69 -0
- package/src/i18n/locales/en.json +1052 -0
- package/src/index.tsx +2336 -0
- package/src/native/NativeInspector.ts +397 -0
- package/src/native/NativeNetworkInspector.ts +24 -0
- package/src/network.ts +22 -0
- package/src/performance.ts +38 -0
- package/src/redux.ts +23 -0
- package/src/styles/AppColors.ts +408 -0
- package/src/styles/AppFonts.ts +8 -0
- package/src/styles/common.ts +209 -0
- package/src/styles/index.ts +1570 -0
- package/src/types/enums.ts +194 -0
- package/src/types/index.ts +34 -0
- package/src/types/interfaces.ts +515 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// #8 — Redux inspector module.
|
|
2
|
+
//
|
|
3
|
+
// Two integration paths, both feeding the same timeline/state snapshot:
|
|
4
|
+
//
|
|
5
|
+
// 1. `inspectorReduxMiddleware` (recommended) — a standard Redux middleware.
|
|
6
|
+
// Because it sits inside the middleware chain it sees EVERY action,
|
|
7
|
+
// including ones dispatched from thunks, sagas, listeners and RTK Query.
|
|
8
|
+
//
|
|
9
|
+
// 2. `connectReduxStore(store)` — zero-config fallback. It wraps the outer
|
|
10
|
+
// `store.dispatch` AND diffs state on `store.subscribe`, so even actions
|
|
11
|
+
// that bypass the wrapped dispatch (thunk/saga internals capture the raw
|
|
12
|
+
// dispatch reference at store-creation time) still update the state tree
|
|
13
|
+
// and per-reducer "last action" consistently.
|
|
14
|
+
|
|
15
|
+
import {ReduxHistoryEntry} from '../types';
|
|
16
|
+
import {parseStackLine} from '../helpers';
|
|
17
|
+
|
|
18
|
+
export {ReduxHistoryEntry};
|
|
19
|
+
|
|
20
|
+
let currentReduxState: any = null;
|
|
21
|
+
const listeners = new Set<() => void>();
|
|
22
|
+
let globalReduxAutoRefresh = true;
|
|
23
|
+
let isReduxModuleEnabled = false;
|
|
24
|
+
|
|
25
|
+
export const setReduxModuleEnabled = (enabled: boolean) => {
|
|
26
|
+
isReduxModuleEnabled = enabled;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getReduxModuleEnabled = () => isReduxModuleEnabled;
|
|
30
|
+
|
|
31
|
+
let lastActionForReducer: Record<string, any> = {};
|
|
32
|
+
|
|
33
|
+
let actionHistory: ReduxHistoryEntry[] = [];
|
|
34
|
+
|
|
35
|
+
let maxReduxHistoryLimit = 50;
|
|
36
|
+
let historyIdSeq = 0;
|
|
37
|
+
|
|
38
|
+
export const setMaxReduxHistoryLimit = (limit: number): void => {
|
|
39
|
+
maxReduxHistoryLimit = Math.max(10, limit);
|
|
40
|
+
if (actionHistory.length > maxReduxHistoryLimit) {
|
|
41
|
+
actionHistory = actionHistory.slice(0, maxReduxHistoryLimit);
|
|
42
|
+
notify();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const getMaxReduxHistoryLimit = (): number => maxReduxHistoryLimit;
|
|
47
|
+
|
|
48
|
+
export const pruneReduxHistory = (targetCount?: number): number => {
|
|
49
|
+
const countToKeep = targetCount !== undefined ? Math.max(0, targetCount) : Math.floor(actionHistory.length / 2);
|
|
50
|
+
const pruned = actionHistory.length - countToKeep;
|
|
51
|
+
if (pruned > 0) {
|
|
52
|
+
actionHistory = actionHistory.slice(0, countToKeep);
|
|
53
|
+
notify();
|
|
54
|
+
}
|
|
55
|
+
return Math.max(0, pruned);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Guards against double-instrumentation (e.g. connectReduxStore called twice,
|
|
59
|
+
// or middleware + connect used together) which previously produced duplicate
|
|
60
|
+
// timeline entries and inconsistent counts.
|
|
61
|
+
const connectedStores = new WeakSet<object>();
|
|
62
|
+
let middlewareAttached = false;
|
|
63
|
+
|
|
64
|
+
export const isReduxConnected = () =>
|
|
65
|
+
middlewareAttached || currentReduxState != null || actionHistory.length > 0;
|
|
66
|
+
|
|
67
|
+
export const getReduxState = () => currentReduxState;
|
|
68
|
+
|
|
69
|
+
export const setReduxAutoRefresh = (val: boolean) => {
|
|
70
|
+
globalReduxAutoRefresh = val;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const getReduxAutoRefresh = () => globalReduxAutoRefresh;
|
|
74
|
+
|
|
75
|
+
export const getLastActionForReducer = () => lastActionForReducer;
|
|
76
|
+
|
|
77
|
+
export const clearLastActionForReducer = () => {
|
|
78
|
+
lastActionForReducer = {};
|
|
79
|
+
notify();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const getActionHistory = () => actionHistory;
|
|
83
|
+
|
|
84
|
+
export const clearActionHistory = () => {
|
|
85
|
+
actionHistory = [];
|
|
86
|
+
notify();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const setReduxState = (state: any) => {
|
|
90
|
+
currentReduxState = state;
|
|
91
|
+
notify();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const subscribeReduxState = (cb: () => void) => {
|
|
95
|
+
listeners.add(cb);
|
|
96
|
+
return () => {
|
|
97
|
+
listeners.delete(cb);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function notify() {
|
|
102
|
+
listeners.forEach(cb => cb());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function actionTypeOf(action: any): string {
|
|
106
|
+
if (typeof action === 'string') return action;
|
|
107
|
+
if (action && typeof action === 'object' && action.type != null) {
|
|
108
|
+
return String(action.type);
|
|
109
|
+
}
|
|
110
|
+
if (typeof action === 'function') {
|
|
111
|
+
return action.name ? `thunk: ${action.name}` : 'thunk';
|
|
112
|
+
}
|
|
113
|
+
return 'UNKNOWN_ACTION';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function detectActionOrigin(stack: string, actionType: string, actionObj?: any): {
|
|
117
|
+
originType: 'saga' | 'thunk' | 'ui' | 'direct' | 'listener';
|
|
118
|
+
caller?: string;
|
|
119
|
+
callerFile?: string;
|
|
120
|
+
callerLine?: number;
|
|
121
|
+
callerCol?: number;
|
|
122
|
+
} {
|
|
123
|
+
const stackLower = (stack || '').toLowerCase();
|
|
124
|
+
const typeLower = (actionType || '').toLowerCase();
|
|
125
|
+
|
|
126
|
+
// Explicit hint on action if developer tagged it
|
|
127
|
+
if (actionObj && actionObj.__origin) {
|
|
128
|
+
const raw = String(actionObj.__origin).toLowerCase();
|
|
129
|
+
if (raw.includes('saga')) return {originType: 'saga'};
|
|
130
|
+
if (raw.includes('thunk')) return {originType: 'thunk'};
|
|
131
|
+
if (raw.includes('ui')) return {originType: 'ui'};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. Saga Detection (Stack + Action naming conventions)
|
|
135
|
+
const isSaga =
|
|
136
|
+
stackLower.includes('redux-saga') ||
|
|
137
|
+
stackLower.includes('runputeffect') ||
|
|
138
|
+
stackLower.includes('proc.js') ||
|
|
139
|
+
stackLower.includes('effects.js') ||
|
|
140
|
+
stackLower.includes('sagamiddleware') ||
|
|
141
|
+
stackLower.includes('saga.ts') ||
|
|
142
|
+
stackLower.includes('saga.js') ||
|
|
143
|
+
stackLower.includes('sagas') ||
|
|
144
|
+
typeLower.includes('saga') ||
|
|
145
|
+
typeLower.startsWith('saga/') ||
|
|
146
|
+
typeLower.startsWith('@saga/');
|
|
147
|
+
|
|
148
|
+
// 2. Thunk / RTK Query Detection
|
|
149
|
+
const isThunk =
|
|
150
|
+
stackLower.includes('createasyncthunk') ||
|
|
151
|
+
stackLower.includes('redux-thunk') ||
|
|
152
|
+
stackLower.includes('rtk-query') ||
|
|
153
|
+
stackLower.includes('thunkmiddleware') ||
|
|
154
|
+
stackLower.includes('actioncreator') ||
|
|
155
|
+
typeLower.includes('thunk') ||
|
|
156
|
+
typeLower.endsWith('/pending') ||
|
|
157
|
+
typeLower.endsWith('/fulfilled') ||
|
|
158
|
+
typeLower.endsWith('/rejected');
|
|
159
|
+
|
|
160
|
+
// 3. Parse Stack Frames to find best application caller
|
|
161
|
+
const lines = (stack || '').split('\n').map(l => l.trim()).filter(Boolean);
|
|
162
|
+
let bestFrame: any = null;
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < lines.length; i++) {
|
|
165
|
+
const line = lines[i];
|
|
166
|
+
// Ignore logger / middleware lines inside inspector
|
|
167
|
+
if (
|
|
168
|
+
line.includes('reduxLogger') ||
|
|
169
|
+
line.includes('inspectorReduxMiddleware') ||
|
|
170
|
+
line.includes('recordAction') ||
|
|
171
|
+
line.includes('redux.js') ||
|
|
172
|
+
line.includes('redux.cjs')
|
|
173
|
+
) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const parsed = parseStackLine(line, false);
|
|
177
|
+
if (parsed.frameType === 'app' || (!parsed.isRuntimeNoise && parsed.fileName !== 'Unknown')) {
|
|
178
|
+
bestFrame = parsed;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!bestFrame && lines.length > 0) {
|
|
184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
185
|
+
if (!lines[i].includes('reduxLogger') && !lines[i].includes('inspectorReduxMiddleware')) {
|
|
186
|
+
bestFrame = parseStackLine(lines[i], false);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let originType: 'saga' | 'thunk' | 'ui' | 'direct' | 'listener' = 'direct';
|
|
193
|
+
if (isSaga || (bestFrame && bestFrame.fileName?.toLowerCase().includes('saga'))) {
|
|
194
|
+
originType = 'saga';
|
|
195
|
+
} else if (isThunk || (bestFrame && bestFrame.fileName?.toLowerCase().includes('thunk'))) {
|
|
196
|
+
originType = 'thunk';
|
|
197
|
+
} else if (
|
|
198
|
+
bestFrame &&
|
|
199
|
+
(bestFrame.fileName?.endsWith('.tsx') ||
|
|
200
|
+
bestFrame.functionName?.includes('onPress') ||
|
|
201
|
+
bestFrame.functionName?.includes('touchable') ||
|
|
202
|
+
bestFrame.functionName?.includes('Effect'))
|
|
203
|
+
) {
|
|
204
|
+
originType = 'ui';
|
|
205
|
+
} else if (stackLower.includes('listener') || stackLower.includes('subscribe')) {
|
|
206
|
+
originType = 'listener';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
originType,
|
|
211
|
+
caller: bestFrame?.copyableLocation || bestFrame?.raw || undefined,
|
|
212
|
+
callerFile: bestFrame?.fullPath || bestFrame?.fileName || undefined,
|
|
213
|
+
callerLine: bestFrame?.lineNumber ? Number(bestFrame.lineNumber) : undefined,
|
|
214
|
+
callerCol: bestFrame?.columnNumber ? Number(bestFrame.columnNumber) : undefined,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function extractSliceName(actionType: string, affectedSlices: string[]): string | undefined {
|
|
219
|
+
if (affectedSlices && affectedSlices.length > 0) {
|
|
220
|
+
return affectedSlices[0];
|
|
221
|
+
}
|
|
222
|
+
// If Redux Toolkit action format: e.g. "users/fetchById" -> "users"
|
|
223
|
+
const match = actionType.match(/^([a-zA-Z0-9_\-]+)\//);
|
|
224
|
+
if (match && match[1] && !match[1].startsWith('@@')) {
|
|
225
|
+
return match[1];
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function recordAction(action: any, prevState: any, nextState: any, rawStack?: string) {
|
|
231
|
+
if (!isReduxModuleEnabled) return;
|
|
232
|
+
const stack = rawStack || new Error().stack || '';
|
|
233
|
+
const type = actionTypeOf(action);
|
|
234
|
+
const payload =
|
|
235
|
+
action && typeof action === 'object' && action.payload !== undefined
|
|
236
|
+
? action.payload
|
|
237
|
+
: null;
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
const timestamp = new Date(now).toLocaleTimeString();
|
|
240
|
+
|
|
241
|
+
const affectedSlices: string[] = [];
|
|
242
|
+
const origin = detectActionOrigin(stack, type, action);
|
|
243
|
+
const sliceName = extractSliceName(type, affectedSlices);
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
prevState &&
|
|
247
|
+
nextState &&
|
|
248
|
+
typeof prevState === 'object' &&
|
|
249
|
+
typeof nextState === 'object'
|
|
250
|
+
) {
|
|
251
|
+
Object.keys(nextState).forEach(key => {
|
|
252
|
+
if (prevState[key] !== nextState[key]) {
|
|
253
|
+
lastActionForReducer[key] = {
|
|
254
|
+
type,
|
|
255
|
+
payload,
|
|
256
|
+
timestamp,
|
|
257
|
+
updatedAt: now,
|
|
258
|
+
originType: origin.originType,
|
|
259
|
+
callerFile: origin.callerFile,
|
|
260
|
+
callerLine: origin.callerLine,
|
|
261
|
+
callerCol: origin.callerCol,
|
|
262
|
+
caller: origin.caller,
|
|
263
|
+
stack,
|
|
264
|
+
};
|
|
265
|
+
affectedSlices.push(key);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
actionHistory.unshift({
|
|
271
|
+
id: ++historyIdSeq,
|
|
272
|
+
type,
|
|
273
|
+
payload,
|
|
274
|
+
timestamp,
|
|
275
|
+
updatedAt: now,
|
|
276
|
+
affectedSlices,
|
|
277
|
+
prevState,
|
|
278
|
+
nextState,
|
|
279
|
+
stack,
|
|
280
|
+
caller: origin.caller,
|
|
281
|
+
callerFile: origin.callerFile,
|
|
282
|
+
callerLine: origin.callerLine,
|
|
283
|
+
callerCol: origin.callerCol,
|
|
284
|
+
originType: origin.originType,
|
|
285
|
+
sliceName,
|
|
286
|
+
});
|
|
287
|
+
if (actionHistory.length > maxReduxHistoryLimit) {
|
|
288
|
+
actionHistory.length = maxReduxHistoryLimit;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (globalReduxAutoRefresh) {
|
|
292
|
+
currentReduxState = nextState;
|
|
293
|
+
}
|
|
294
|
+
// Timeline / last-action map always changed — notify even when the state
|
|
295
|
+
// tree snapshot is paused so those panels stay live.
|
|
296
|
+
notify();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Standard Redux middleware — add it to your store to capture every action,
|
|
301
|
+
* including those dispatched from thunks, sagas and RTK Query:
|
|
302
|
+
*
|
|
303
|
+
* const store = configureStore({
|
|
304
|
+
* reducer,
|
|
305
|
+
* middleware: gDM => gDM().concat(inspectorReduxMiddleware),
|
|
306
|
+
* });
|
|
307
|
+
*
|
|
308
|
+
* Pair with `connectReduxStore(store)` (safe — they de-duplicate) or rely on
|
|
309
|
+
* the middleware alone; the initial snapshot is taken on the first action.
|
|
310
|
+
*/
|
|
311
|
+
export const inspectorReduxMiddleware =
|
|
312
|
+
(storeApi: any) => (next: (action: any) => any) => (action: any) => {
|
|
313
|
+
middlewareAttached = true;
|
|
314
|
+
const capturedStack = new Error().stack || '';
|
|
315
|
+
// Thunks are functions — let them run; their inner plain-action dispatches
|
|
316
|
+
// pass back through this same middleware, so nothing is lost.
|
|
317
|
+
if (typeof action === 'function') {
|
|
318
|
+
return next(action);
|
|
319
|
+
}
|
|
320
|
+
const prevState = storeApi.getState();
|
|
321
|
+
const result = next(action);
|
|
322
|
+
const nextState = storeApi.getState();
|
|
323
|
+
if (currentReduxState == null) currentReduxState = nextState;
|
|
324
|
+
recordAction(action, prevState, nextState, capturedStack);
|
|
325
|
+
return result;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const connectReduxStore = (store: any) => {
|
|
329
|
+
if (
|
|
330
|
+
!store ||
|
|
331
|
+
typeof store.getState !== 'function' ||
|
|
332
|
+
typeof store.subscribe !== 'function'
|
|
333
|
+
) {
|
|
334
|
+
console.warn(
|
|
335
|
+
'[NetworkInspector] Invalid Redux store passed to connectReduxStore.',
|
|
336
|
+
);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Idempotent — connecting the same store twice must not double-wrap
|
|
341
|
+
// dispatch or double-record the timeline.
|
|
342
|
+
if (connectedStores.has(store)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
connectedStores.add(store);
|
|
346
|
+
|
|
347
|
+
// Wrap the outer dispatch so directly dispatched actions get full
|
|
348
|
+
// type/payload attribution. Skipped when the middleware is already
|
|
349
|
+
// attached, otherwise every direct dispatch would be recorded twice.
|
|
350
|
+
const originalDispatch = store.dispatch.bind(store);
|
|
351
|
+
let inWrappedDispatch = false;
|
|
352
|
+
store.dispatch = (action: any) => {
|
|
353
|
+
const capturedStack = new Error().stack || '';
|
|
354
|
+
if (middlewareAttached || typeof action === 'function') {
|
|
355
|
+
// Middleware handles recording, or it's a thunk whose inner dispatches
|
|
356
|
+
// will be picked up individually.
|
|
357
|
+
inWrappedDispatch = true;
|
|
358
|
+
try {
|
|
359
|
+
return originalDispatch(action);
|
|
360
|
+
} finally {
|
|
361
|
+
inWrappedDispatch = false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const prevState = store.getState();
|
|
365
|
+
inWrappedDispatch = true;
|
|
366
|
+
let result;
|
|
367
|
+
try {
|
|
368
|
+
result = originalDispatch(action);
|
|
369
|
+
} finally {
|
|
370
|
+
inWrappedDispatch = false;
|
|
371
|
+
}
|
|
372
|
+
recordAction(action, prevState, store.getState(), capturedStack);
|
|
373
|
+
return result;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
setReduxState(store.getState());
|
|
377
|
+
|
|
378
|
+
// Subscribe-diff fallback: catches state changes whose dispatch bypassed
|
|
379
|
+
// the wrapper above (thunk/saga internals hold the raw dispatch reference).
|
|
380
|
+
// Without this, the tree and per-reducer last-action drifted out of sync.
|
|
381
|
+
let lastSeenState = store.getState();
|
|
382
|
+
store.subscribe(() => {
|
|
383
|
+
const nextState = store.getState();
|
|
384
|
+
if (nextState === lastSeenState) return;
|
|
385
|
+
const prevState = lastSeenState;
|
|
386
|
+
lastSeenState = nextState;
|
|
387
|
+
|
|
388
|
+
if (inWrappedDispatch || middlewareAttached) {
|
|
389
|
+
// Already recorded with proper attribution; just refresh the snapshot.
|
|
390
|
+
if (globalReduxAutoRefresh) {
|
|
391
|
+
currentReduxState = nextState;
|
|
392
|
+
notify();
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// Change arrived outside the wrapped dispatch — record it so the
|
|
397
|
+
// timeline stays consistent, even without the original action type.
|
|
398
|
+
const subStack = new Error().stack || '';
|
|
399
|
+
recordAction(
|
|
400
|
+
{type: '@@inspector/EXTERNAL_STATE_CHANGE'},
|
|
401
|
+
prevState,
|
|
402
|
+
nextState,
|
|
403
|
+
subStack,
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {useCallback, useRef, useState} from 'react';
|
|
2
|
+
import {Animated} from 'react-native';
|
|
3
|
+
|
|
4
|
+
const useAccordion = (
|
|
5
|
+
initialOpen = false,
|
|
6
|
+
maxBodyHeight = 1200,
|
|
7
|
+
duration = 280,
|
|
8
|
+
) => {
|
|
9
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
10
|
+
const animVal = useRef(new Animated.Value(initialOpen ? 1 : 0)).current;
|
|
11
|
+
|
|
12
|
+
const toggleOpen = useCallback(() => {
|
|
13
|
+
const next = !isOpen;
|
|
14
|
+
setIsOpen(next);
|
|
15
|
+
Animated.timing(animVal, {
|
|
16
|
+
toValue: next ? 1 : 0,
|
|
17
|
+
duration,
|
|
18
|
+
useNativeDriver: false,
|
|
19
|
+
}).start();
|
|
20
|
+
}, [isOpen, animVal, duration]);
|
|
21
|
+
|
|
22
|
+
const forceOpen = useCallback(
|
|
23
|
+
(open: boolean) => {
|
|
24
|
+
setIsOpen(open);
|
|
25
|
+
Animated.timing(animVal, {
|
|
26
|
+
toValue: open ? 1 : 0,
|
|
27
|
+
duration,
|
|
28
|
+
useNativeDriver: false,
|
|
29
|
+
}).start();
|
|
30
|
+
},
|
|
31
|
+
[animVal, duration],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const chevronStyle = {
|
|
35
|
+
transform: [
|
|
36
|
+
{
|
|
37
|
+
rotate: animVal.interpolate({
|
|
38
|
+
inputRange: [0, 1],
|
|
39
|
+
outputRange: ['0deg', '180deg'],
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const bodyStyle = {
|
|
46
|
+
maxHeight: animVal.interpolate({
|
|
47
|
+
inputRange: [0, 1],
|
|
48
|
+
outputRange: [0, maxBodyHeight],
|
|
49
|
+
}),
|
|
50
|
+
overflow: 'hidden' as const,
|
|
51
|
+
opacity: animVal.interpolate({
|
|
52
|
+
inputRange: [0, 0.4, 1],
|
|
53
|
+
outputRange: [0, 0.7, 1],
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {isOpen, toggleOpen, forceOpen, chevronStyle, bodyStyle};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default useAccordion;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// ─── Method & Class Decorators for React Native In-App Inspector ─────────────
|
|
2
|
+
//
|
|
3
|
+
// Clean TypeScript decorators to seamlessly annotate methods, asynchronous actions,
|
|
4
|
+
// and API services with automatic inspection, logging, timing, and error boundaries.
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface InspectLogOptions {
|
|
8
|
+
tag?: string;
|
|
9
|
+
logArgs?: boolean;
|
|
10
|
+
logResult?: boolean;
|
|
11
|
+
logDuration?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `@InspectLog`: Automatically logs method execution, arguments, duration, and return values
|
|
16
|
+
* to the In-App Inspector Console.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* class UserService {
|
|
21
|
+
* @InspectLog({ tag: 'API', logArgs: true, logDuration: true })
|
|
22
|
+
* async fetchUser(id: string) {
|
|
23
|
+
* return api.get(`/users/${id}`);
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function InspectLog(options: InspectLogOptions = {}) {
|
|
29
|
+
const {
|
|
30
|
+
tag = 'Inspect',
|
|
31
|
+
logArgs = true,
|
|
32
|
+
logResult = true,
|
|
33
|
+
logDuration = true,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
return function (
|
|
37
|
+
_target: any,
|
|
38
|
+
propertyKey: string,
|
|
39
|
+
descriptor: PropertyDescriptor,
|
|
40
|
+
) {
|
|
41
|
+
const originalMethod = descriptor.value;
|
|
42
|
+
|
|
43
|
+
descriptor.value = function (...args: any[]) {
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const prefix = `[${tag}] ${propertyKey}`;
|
|
46
|
+
|
|
47
|
+
if (logArgs && args.length > 0) {
|
|
48
|
+
console.log(`${prefix} called with arguments:`, ...args);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(`${prefix} invoked`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = originalMethod.apply(this, args);
|
|
55
|
+
|
|
56
|
+
if (result && typeof result.then === 'function') {
|
|
57
|
+
return result
|
|
58
|
+
.then((asyncResult: any) => {
|
|
59
|
+
const elapsed = Date.now() - start;
|
|
60
|
+
if (logResult) {
|
|
61
|
+
console.log(
|
|
62
|
+
`${prefix} resolved${logDuration ? ` in ${elapsed}ms` : ''}:`,
|
|
63
|
+
asyncResult,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return asyncResult;
|
|
67
|
+
})
|
|
68
|
+
.catch((err: any) => {
|
|
69
|
+
const elapsed = Date.now() - start;
|
|
70
|
+
console.error(
|
|
71
|
+
`${prefix} rejected${logDuration ? ` in ${elapsed}ms` : ''}:`,
|
|
72
|
+
err,
|
|
73
|
+
);
|
|
74
|
+
throw err;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const elapsed = Date.now() - start;
|
|
79
|
+
if (logResult) {
|
|
80
|
+
console.log(
|
|
81
|
+
`${prefix} returned${logDuration ? ` in ${elapsed}ms` : ''}:`,
|
|
82
|
+
result,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const elapsed = Date.now() - start;
|
|
88
|
+
console.error(
|
|
89
|
+
`${prefix} failed${logDuration ? ` in ${elapsed}ms` : ''}:`,
|
|
90
|
+
error,
|
|
91
|
+
);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return descriptor;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `@InspectTrackTime`: Measures and logs execution duration for critical functions.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* class FeedManager {
|
|
106
|
+
* @InspectTrackTime('Feed Data Processing')
|
|
107
|
+
* processFeedItems(items: FeedItem[]) { ... }
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function InspectTrackTime(label?: string) {
|
|
112
|
+
return function (
|
|
113
|
+
_target: any,
|
|
114
|
+
propertyKey: string,
|
|
115
|
+
descriptor: PropertyDescriptor,
|
|
116
|
+
) {
|
|
117
|
+
const originalMethod = descriptor.value;
|
|
118
|
+
const metricLabel = label || propertyKey;
|
|
119
|
+
|
|
120
|
+
descriptor.value = function (...args: any[]) {
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
try {
|
|
123
|
+
const result = originalMethod.apply(this, args);
|
|
124
|
+
if (result && typeof result.then === 'function') {
|
|
125
|
+
return result.finally(() => {
|
|
126
|
+
const duration = Date.now() - start;
|
|
127
|
+
console.log(`[PERF] ⚡ ${metricLabel} took ${duration}ms`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const duration = Date.now() - start;
|
|
131
|
+
console.log(`[PERF] ⚡ ${metricLabel} took ${duration}ms`);
|
|
132
|
+
return result;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const duration = Date.now() - start;
|
|
135
|
+
console.error(`[PERF] ⚡ ${metricLabel} failed after ${duration}ms:`, err);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return descriptor;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `@InspectCatch`: Safely catches unhandled exceptions thrown inside the method,
|
|
146
|
+
* logs them to the In-App Inspector, and returns a fallback value.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* class AuthService {
|
|
151
|
+
* @InspectCatch({ fallback: null, tag: 'AUTH' })
|
|
152
|
+
* async getCachedToken() { ... }
|
|
153
|
+
* }
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function InspectCatch(options: { fallback?: any; tag?: string } = {}) {
|
|
157
|
+
const { fallback = undefined, tag = 'Catch' } = options;
|
|
158
|
+
|
|
159
|
+
return function (
|
|
160
|
+
_target: any,
|
|
161
|
+
propertyKey: string,
|
|
162
|
+
descriptor: PropertyDescriptor,
|
|
163
|
+
) {
|
|
164
|
+
const originalMethod = descriptor.value;
|
|
165
|
+
|
|
166
|
+
descriptor.value = function (...args: any[]) {
|
|
167
|
+
try {
|
|
168
|
+
const result = originalMethod.apply(this, args);
|
|
169
|
+
if (result && typeof result.then === 'function') {
|
|
170
|
+
return result.catch((err: any) => {
|
|
171
|
+
console.error(`[${tag}] Error caught in ${propertyKey}:`, err);
|
|
172
|
+
return fallback;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(`[${tag}] Error caught in ${propertyKey}:`, err);
|
|
178
|
+
return fallback;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return descriptor;
|
|
183
|
+
};
|
|
184
|
+
}
|