react-native-debug-toolkit 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -3,9 +3,18 @@
3
3
  * A comprehensive debugging toolkit for React Native
4
4
  */
5
5
  import DebugToolKit from './lib/DebugToolKit'
6
+ import { initializeDebugToolkit } from './lib'
7
+
6
8
  import { createNetworkFeature } from './lib/features/NetworkFeature'
9
+ import { createPerformanceFeature } from './lib/features/PerformanceFeature'
10
+ import { createConsoleLogFeature } from './lib/features/ConsoleLogFeature'
7
11
 
8
- export { DebugToolKit, createNetworkFeature }
12
+ export {
13
+ DebugToolKit,
14
+ initializeDebugToolkit,
15
+ createNetworkFeature,
16
+ createPerformanceFeature,
17
+ createConsoleLogFeature,
18
+ }
9
19
 
10
20
  export default DebugToolKit
11
-
@@ -3,34 +3,42 @@ import { BackHandler, Pressable, Text } from 'react-native'
3
3
  import RootSiblings from 'react-native-root-siblings'
4
4
  import FloatPanelView from './views/FloatPanelView'
5
5
 
6
- class DebugToolKit {
6
+ export default class DebugToolKit {
7
7
  static instance = null
8
8
 
9
- constructor() {
9
+ constructor(config = {}) {
10
10
  if (DebugToolKit.instance) {
11
11
  return DebugToolKit.instance
12
12
  }
13
13
 
14
14
  this.floatPanel = null
15
15
  this.features = []
16
+ this.isPanelVisible = false
17
+
18
+ const featuresToLoad = config.features || []
19
+
20
+ featuresToLoad.forEach(featureOrCreator => {
21
+ const feature = typeof featureOrCreator === 'function'
22
+ ? featureOrCreator()
23
+ : featureOrCreator
24
+ this.addFeature(feature)
25
+ })
26
+
27
+ if (config.autoShow) {
28
+ this.showDebugPanel()
29
+ }
16
30
 
17
31
  BackHandler.addEventListener('hardwareBackPress', () => true)
18
32
  DebugToolKit.instance = this
19
33
  }
20
34
 
21
35
  addFeature(feature) {
22
- if (!__DEV__) {
23
- return this
24
- }
25
-
26
- if (!feature.name || !feature.label || !feature.setup || !feature.getData) {
27
- console.error('Invalid feature format', feature)
28
- return this
36
+ if (feature && typeof feature.name === 'string') {
37
+ this.features.push(feature)
38
+ feature.setup?.()
39
+ } else {
40
+ console.warn('[DebugToolKit] Invalid feature added:', feature)
29
41
  }
30
-
31
- feature.setup()
32
- this.features.push(feature)
33
- this.updateDebugPanel()
34
42
  return this
35
43
  }
36
44
 
@@ -44,6 +52,7 @@ class DebugToolKit {
44
52
  <FloatPanelView features={this.features} close={this.hideDebugPanel} />,
45
53
  )
46
54
  }
55
+ this.isPanelVisible = true
47
56
  }
48
57
 
49
58
  hideDebugPanel = () => {
@@ -79,5 +88,3 @@ class DebugToolKit {
79
88
  BackHandler.removeEventListener('hardwareBackPress')
80
89
  }
81
90
  }
82
-
83
- export default DebugToolKit
@@ -0,0 +1,73 @@
1
+ const MAX_LOGS = 100; // Max number of console logs to store
2
+ const logs = [];
3
+ const originalConsole = {}; // Store original console methods
4
+
5
+ const _interceptConsole = () => {
6
+ const levels = ['log', 'info', 'warn', 'error'];
7
+ levels.forEach(level => {
8
+ if (typeof console[level] === 'function') { // Check if it's actually a function
9
+ originalConsole[level] = console[level]; // Store original
10
+
11
+ console[level] = (...args) => {
12
+ // Call original console method first
13
+ originalConsole[level].apply(console, args); // Use apply for proper context
14
+
15
+ // Add log entry
16
+ if (logs.length >= MAX_LOGS) {
17
+ logs.shift(); // Remove the oldest log if limit is reached
18
+ }
19
+
20
+ // Store log data
21
+ logs.push({
22
+ timestamp: new Date(),
23
+ level: level,
24
+ data: args, // Store all arguments passed to console[level]
25
+ });
26
+
27
+ // TODO: Notify UI if needed
28
+ };
29
+ }
30
+ });
31
+ };
32
+
33
+ const _restoreConsole = () => {
34
+ Object.keys(originalConsole).forEach(level => {
35
+ if (originalConsole[level]) {
36
+ console[level] = originalConsole[level];
37
+ delete originalConsole[level]; // Clean up stored method
38
+ }
39
+ });
40
+ };
41
+
42
+ const setup = () => {
43
+ if (!__DEV__) {
44
+ return; // No need to return 'this' in a functional approach
45
+ }
46
+ _interceptConsole();
47
+ };
48
+
49
+ const getData = () => {
50
+ return logs;
51
+ };
52
+
53
+ const cleanup = () => {
54
+ _restoreConsole();
55
+ logs.length = 0; // More efficient way to clear array
56
+ };
57
+
58
+ // Factory function remains similar but uses the module-level functions
59
+ export const createConsoleLogFeature = () => {
60
+ // Ensure setup is only called once if multiple features might be created (though unlikely for console)
61
+ // A simple flag could work here if needed, but maybe setup belongs in the main toolkit init.
62
+ return {
63
+ name: 'console',
64
+ label: 'Console Logs',
65
+ setup: setup, // Reference module-level function
66
+ getData: getData, // Reference module-level function
67
+ cleanup: cleanup, // Reference module-level function
68
+ };
69
+ };
70
+
71
+ // Note: This refactor removes the class structure entirely, relying on module scope
72
+ // for the "singleton" nature. Decide if this fits the overall pattern of other features.
73
+ // The original class-based singleton is also perfectly valid.
@@ -0,0 +1,390 @@
1
+ // import SubViewPerformance from '../views/SubViewPerformance';
2
+ import performance, {
3
+ setResourceLoggingEnabled,
4
+ PerformanceObserver
5
+ } from 'react-native-performance';
6
+
7
+ class PerformanceFeature {
8
+ static instance = null;
9
+ static MAX_ENTRIES = 100;
10
+
11
+ // Native mark names based on vanilla example
12
+ static NATIVE_MARKS = {
13
+ NATIVE_LAUNCH_START: 'nativeLaunchStart',
14
+ NATIVE_LAUNCH_END: 'nativeLaunchEnd',
15
+ RUN_JS_BUNDLE_START: 'runJsBundleStart',
16
+ RUN_JS_BUNDLE_END: 'runJsBundleEnd',
17
+ CONTENT_APPEARED: 'contentAppeared'
18
+ };
19
+
20
+ constructor() {
21
+ if (PerformanceFeature.instance) {
22
+ return PerformanceFeature.instance;
23
+ }
24
+
25
+ this.name = 'performance';
26
+ this.label = 'Performance';
27
+ this.performanceData = {
28
+ measures: [],
29
+ metrics: [],
30
+ resources: [],
31
+ nativeMarks: []
32
+ };
33
+ this.observers = [];
34
+ this.isSetup = false;
35
+ this.thresholds = new Map(); // Performance thresholds for alerts
36
+ this.timeOrigin = performance.timeOrigin;
37
+ this.resourceLoggingEnabled = false;
38
+
39
+ PerformanceFeature.instance = this;
40
+ }
41
+
42
+ setup(options = {}) {
43
+ if (!__DEV__ || this.isSetup) {
44
+ return this;
45
+ }
46
+
47
+ const { enableResourceLogging = true } = options;
48
+
49
+ // Enable resource logging if specified
50
+ if (enableResourceLogging) {
51
+ setResourceLoggingEnabled(true);
52
+ this.resourceLoggingEnabled = true;
53
+ }
54
+
55
+ // Mark when the feature is initialized
56
+ performance.mark('DebugToolkit.Init');
57
+
58
+ // Setup observers for different entry types
59
+ this._setupObservers();
60
+
61
+ // Measure app launch metrics
62
+ this._setupAppLaunchMeasurements();
63
+
64
+ this.isSetup = true;
65
+ return this;
66
+ }
67
+
68
+ _setupObservers() {
69
+ // Use the same pattern as in the vanilla example App.tsx
70
+ const setupObserver = (type, dataKey) => {
71
+ try {
72
+ const observer = new PerformanceObserver((list) => {
73
+ // Get entries and sort them by startTime as in the example
74
+ const entries = list.getEntries().sort((a, b) => a.startTime - b.startTime);
75
+
76
+ // Store entries
77
+ this._addEntries(dataKey, entries);
78
+
79
+ // Check thresholds for measures
80
+ if (type === 'measure') {
81
+ this._checkThresholds(entries);
82
+ }
83
+ });
84
+
85
+ // Observe with buffered set to true to get existing entries
86
+ observer.observe({ type, buffered: true });
87
+ this.observers.push(observer);
88
+ } catch (e) {
89
+ console.error(`Failed to setup PerformanceObserver for type "${type}":`, e);
90
+ }
91
+ };
92
+
93
+ // Setup observers for all entry types
94
+ setupObserver('mark', 'marks');
95
+ setupObserver('measure', 'measures');
96
+ setupObserver('metric', 'metrics');
97
+ setupObserver('resource', 'resources');
98
+ setupObserver('react-native-mark', 'nativeMarks');
99
+ }
100
+
101
+ _setupAppLaunchMeasurements() {
102
+ // Create a one-time observer for app launch measurements
103
+ try {
104
+ const observer = new PerformanceObserver((list, obs) => {
105
+ const entries = list.getEntries();
106
+ if (entries.some(entry => entry.name === PerformanceFeature.NATIVE_MARKS.RUN_JS_BUNDLE_END)) {
107
+ this._createLaunchMeasurements();
108
+
109
+ obs.disconnect();
110
+ }
111
+ });
112
+ observer.observe({ type: 'react-native-mark', buffered: true });
113
+ } catch (e) {
114
+ console.error('Failed to setup app launch measurements:', e);
115
+ }
116
+ }
117
+
118
+ _createLaunchMeasurements() {
119
+ const {
120
+ NATIVE_LAUNCH_START,
121
+ NATIVE_LAUNCH_END,
122
+ RUN_JS_BUNDLE_START,
123
+ RUN_JS_BUNDLE_END,
124
+ CONTENT_APPEARED
125
+ } = PerformanceFeature.NATIVE_MARKS;
126
+
127
+ // Measure app launch time (native initialization)
128
+ if (this.hasMark(NATIVE_LAUNCH_START) && this.hasMark(NATIVE_LAUNCH_END)) {
129
+ this.measure('nativeLaunch', NATIVE_LAUNCH_START, NATIVE_LAUNCH_END);
130
+ }
131
+
132
+ // Measure JS bundle execution time
133
+ if (this.hasMark(RUN_JS_BUNDLE_START) && this.hasMark(RUN_JS_BUNDLE_END)) {
134
+ this.measure('runJsBundle', RUN_JS_BUNDLE_START, RUN_JS_BUNDLE_END);
135
+ }
136
+
137
+ // Measure time to first render if content appeared mark exists
138
+ if (this.hasMark(NATIVE_LAUNCH_START) && this.hasMark(CONTENT_APPEARED)) {
139
+ this.measure('timeToContent', NATIVE_LAUNCH_START, CONTENT_APPEARED);
140
+ }
141
+ }
142
+
143
+ _addEntries(type, entries) {
144
+ if (!entries || entries.length === 0) return;
145
+
146
+ // Keep only the last MAX_ENTRIES entries
147
+ this.performanceData[type] = [
148
+ ...entries,
149
+ ...this.performanceData[type]
150
+ ].slice(0, PerformanceFeature.MAX_ENTRIES);
151
+ }
152
+
153
+ _checkThresholds(entries) {
154
+ if (!entries || entries.length === 0 || this.thresholds.size === 0) return;
155
+
156
+ entries.forEach(entry => {
157
+ const threshold = this.thresholds.get(entry.name);
158
+ if (threshold && entry.duration > threshold.value) {
159
+ console.warn(`Performance threshold exceeded: ${entry.name} took ${entry.duration}ms (threshold: ${threshold.value}ms)`);
160
+ if (threshold.callback) {
161
+ threshold.callback(entry);
162
+ }
163
+ }
164
+ });
165
+ }
166
+
167
+ hasMark(name) {
168
+ try {
169
+ return performance.getEntriesByName(name, 'mark').length > 0 ||
170
+ performance.getEntriesByName(name, 'react-native-mark').length > 0;
171
+ } catch (e) {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ // Convert performance timestamp to unix epoch timestamp
177
+ // Based on example: Date.now() - performance.timeOrigin + entry.startTime
178
+ getUnixTimestamp(entry) {
179
+ if (!entry || typeof entry.startTime !== 'number') return null;
180
+ return Date.now() - this.timeOrigin + entry.startTime;
181
+ }
182
+
183
+ // Enable or disable resource logging at runtime
184
+ setResourceLoggingEnabled(enabled) {
185
+ if (this.resourceLoggingEnabled !== enabled) {
186
+ setResourceLoggingEnabled(enabled);
187
+ this.resourceLoggingEnabled = enabled;
188
+ }
189
+ return this;
190
+ }
191
+
192
+ // Threshold management
193
+ setThreshold(name, value, callback) {
194
+ this.thresholds.set(name, { value, callback });
195
+ return this;
196
+ }
197
+
198
+ removeThreshold(name) {
199
+ this.thresholds.delete(name);
200
+ return this;
201
+ }
202
+
203
+ clearThresholds() {
204
+ this.thresholds.clear();
205
+ return this;
206
+ }
207
+
208
+ // Mark events (following pattern from vanilla example)
209
+ mark(name, detail = {}) {
210
+ if (!__DEV__ || !this.isSetup) return this;
211
+
212
+ if (typeof detail !== 'object') {
213
+ detail = { value: detail };
214
+ }
215
+
216
+ performance.mark(name, { detail });
217
+ return this;
218
+ }
219
+
220
+ measure(name, startMarkOrOptions, endMark, detail = {}) {
221
+ if (!__DEV__ || !this.isSetup) return this;
222
+
223
+ try {
224
+ if (typeof startMarkOrOptions === 'string' && endMark) {
225
+ performance.measure(name, startMarkOrOptions, endMark, { detail });
226
+ } else if (typeof startMarkOrOptions === 'object') {
227
+ performance.measure(name, startMarkOrOptions);
228
+ } else {
229
+ performance.measure(name, { detail });
230
+ }
231
+ } catch (e) {
232
+ }
233
+
234
+ return this;
235
+ }
236
+
237
+ // Record custom metrics (following pattern from vanilla example)
238
+ metric(name, value, detail = {}) {
239
+ if (!__DEV__ || !this.isSetup) return this;
240
+
241
+ if (typeof detail !== 'object') {
242
+ detail = { info: detail };
243
+ }
244
+
245
+ performance.metric(name, value, { detail });
246
+ return this;
247
+ }
248
+
249
+ // Measure execution time of a function
250
+ measureFunction(name, fn, ...args) {
251
+ if (!__DEV__ || !this.isSetup) {
252
+ return fn(...args);
253
+ }
254
+
255
+ const markName = `${name}_start`;
256
+ this.mark(markName);
257
+
258
+ try {
259
+ const result = fn(...args);
260
+
261
+ // Handle promises
262
+ if (result instanceof Promise) {
263
+ return result.finally(() => {
264
+ this.measure(name, markName);
265
+ });
266
+ }
267
+
268
+ // Handle synchronous functions
269
+ this.measure(name, markName);
270
+ return result;
271
+ } catch (e) {
272
+ this.measure(`${name}_error`, markName, undefined, { error: e.message });
273
+ throw e;
274
+ }
275
+ }
276
+
277
+ // Get formatted performance data
278
+ getData() {
279
+ return this.performanceData;
280
+ }
281
+
282
+ getTimingData(options = {}) {
283
+ const { includeMarks = true, includeMeasures = true, filterByName } = options;
284
+
285
+ let data = [];
286
+
287
+ if (includeMarks) {
288
+ data = [...data, ...this.performanceData.marks, ...this.performanceData.nativeMarks];
289
+ }
290
+
291
+ if (includeMeasures) {
292
+ data = [...data, ...this.performanceData.measures];
293
+ }
294
+
295
+ if (filterByName) {
296
+ if (filterByName instanceof RegExp) {
297
+ data = data.filter(entry => filterByName.test(entry.name));
298
+ } else if (typeof filterByName === 'string') {
299
+ data = data.filter(entry => entry.name.includes(filterByName));
300
+ }
301
+ }
302
+
303
+ return data.sort((a, b) => a.startTime - b.startTime);
304
+ }
305
+
306
+ // Get network resource data (similar to the demo pattern)
307
+ getResourceData() {
308
+ return this.performanceData.resources.sort((a, b) => a.startTime - b.startTime);
309
+ }
310
+
311
+ // Trigger a test network request (for demo purposes)
312
+ generateTestNetworkRequest() {
313
+ if (!__DEV__ || !this.isSetup || !this.resourceLoggingEnabled) return this;
314
+
315
+ // Similar to the fetch in the vanilla example
316
+ fetch('https://jsonplaceholder.typicode.com/todos/1', { cache: 'no-cache' })
317
+ .catch(err => console.error('Test network request failed:', err));
318
+
319
+ return this;
320
+ }
321
+
322
+ // Clear specific type of entries
323
+ clear(type) {
324
+ if (this.performanceData[type]) {
325
+ this.performanceData[type] = [];
326
+ }
327
+ return this;
328
+ }
329
+
330
+ // Clear all performance data
331
+ clearAll() {
332
+ Object.keys(this.performanceData).forEach(key => {
333
+ this.performanceData[key] = [];
334
+ });
335
+ return this;
336
+ }
337
+
338
+ cleanup() {
339
+ // Disconnect all observers
340
+ this.observers.forEach(observer => {
341
+ observer.disconnect();
342
+ });
343
+ this.observers = [];
344
+
345
+ // Disable resource logging if it was enabled
346
+ if (this.resourceLoggingEnabled) {
347
+ setResourceLoggingEnabled(false);
348
+ this.resourceLoggingEnabled = false;
349
+ }
350
+
351
+ // Clear stored data
352
+ this.clearAll();
353
+ this.clearThresholds();
354
+
355
+ this.isSetup = false;
356
+ return this;
357
+ }
358
+ }
359
+
360
+ export const createPerformanceFeature = () => {
361
+ const feature = new PerformanceFeature();
362
+
363
+ return {
364
+ name: feature.name,
365
+ label: feature.label,
366
+ setup: (options) => feature.setup(options),
367
+ getData: () => feature.getData(),
368
+ getTimingData: (options) => feature.getTimingData(options),
369
+ getResourceData: () => feature.getResourceData(),
370
+ getUnixTimestamp: (entry) => feature.getUnixTimestamp(entry),
371
+ hasMark: (name) => feature.hasMark(name),
372
+ setResourceLoggingEnabled: (enabled) => feature.setResourceLoggingEnabled(enabled),
373
+ generateTestNetworkRequest: () => feature.generateTestNetworkRequest(),
374
+ cleanup: () => feature.cleanup(),
375
+ mark: (name, detail) => feature.mark(name, detail),
376
+ measure: (name, startMarkOrOptions, endMark, detail) =>
377
+ feature.measure(name, startMarkOrOptions, endMark, detail),
378
+ metric: (name, value, detail) => feature.metric(name, value, detail),
379
+ measureFunction: (name, fn, ...args) => feature.measureFunction(name, fn, ...args),
380
+ clear: (type) => feature.clear(type),
381
+ clearAll: () => feature.clearAll(),
382
+ setThreshold: (name, value, callback) => feature.setThreshold(name, value, callback),
383
+ removeThreshold: (name) => feature.removeThreshold(name),
384
+ clearThresholds: () => feature.clearThresholds(),
385
+ };
386
+ };
387
+
388
+ // For backward compatibility
389
+ const instance = new PerformanceFeature();
390
+ export default instance;
package/lib/index.js CHANGED
@@ -1,6 +1,76 @@
1
1
  import DebugToolKit from './DebugToolKit'
2
2
  import { createNetworkFeature } from './features/NetworkFeature'
3
+ import { createPerformanceFeature} from './features/PerformanceFeature'
4
+ import { createConsoleLogFeature } from './features/ConsoleLogFeature'
3
5
 
4
- export { DebugToolKit, createNetworkFeature }
6
+ // Registry mapping feature names (strings) to their creator functions
7
+ const featureRegistry = {
8
+ network: createNetworkFeature,
9
+ console: createConsoleLogFeature,
10
+ performance: createPerformanceFeature,
11
+ // Add other built-in features here
12
+ // 'storage': createStorageFeature,
13
+ }
5
14
 
6
- export default DebugToolKit
15
+ // List of default features (can use strings now)
16
+ const defaultFeatures = ['network', 'console']
17
+
18
+ /**
19
+ * Initializes and shows the Debug ToolKit panel with specified features.
20
+ * Only runs in development mode (__DEV__ === true).
21
+ * Features can be specified as strings (e.g., 'network') or creator functions.
22
+ * @param {Array<string|Function>} features - Array of feature names (strings) or creator functions. Defaults to standard features.
23
+ */
24
+ export function initializeDebugToolkit(features = defaultFeatures) {
25
+
26
+ try {
27
+ const debugToolKit = new DebugToolKit();
28
+
29
+ features.forEach(featureIdentifier => {
30
+ let feature = null;
31
+ let featureCreator = null;
32
+
33
+ if (typeof featureIdentifier === 'string') {
34
+ // It's a string, look it up in the registry
35
+ featureCreator = featureRegistry[featureIdentifier];
36
+ if (!featureCreator) {
37
+ console.warn(`[DebugToolKit] Unknown feature identifier string: "${featureIdentifier}". Skipping.`);
38
+ return; // Skip this identifier
39
+ }
40
+ feature = featureCreator();
41
+ } else if (typeof featureIdentifier === 'function') {
42
+ // It's a function, assume it's a creator
43
+ featureCreator = featureIdentifier;
44
+ try {
45
+ feature = featureCreator();
46
+ } catch (e) {
47
+ console.error(`[DebugToolKit] Error calling feature creator function:`, e);
48
+ return; // Skip if creator fails
49
+ }
50
+ } else if (typeof featureIdentifier === 'object' && featureIdentifier !== null && featureIdentifier.name) {
51
+ // It might be a pre-created feature object
52
+ feature = featureIdentifier;
53
+ console.warn(`[DebugToolKit] Passing pre-created feature objects is supported but using strings or creators is recommended.`);
54
+ } else {
55
+ console.warn('[DebugToolKit] Invalid feature identifier type:', typeof featureIdentifier, featureIdentifier, '. Skipping.');
56
+ return; // Skip invalid types
57
+ }
58
+
59
+ if (feature) {
60
+ debugToolKit.addFeature(feature);
61
+ } else {
62
+ // This case might happen if a creator function returns null/undefined
63
+ console.warn(`[DebugToolKit] Feature creator for identifier "${typeof featureIdentifier === 'string' ? featureIdentifier : 'custom function'}" did not return a valid feature object.`);
64
+ }
65
+ });
66
+
67
+ debugToolKit.showDebugPanel();
68
+ return debugToolKit; // Return instance if needed elsewhere
69
+ } catch (error) {
70
+ console.error('[DebugToolKit] Failed to initialize:', error);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ // Export existing stuff and the new initializer
76
+ export { DebugToolKit, createNetworkFeature, createPerformanceFeature, createConsoleLogFeature, featureRegistry };
@@ -16,7 +16,20 @@ export const DebugColors = {
16
16
  error: '#FF3B30',
17
17
  warning: '#FF9500'
18
18
  };
19
-
19
+ export const getLogLevelColor = (level) => {
20
+ switch (level?.toLowerCase()) {
21
+ case 'log':
22
+ return '#666';
23
+ case 'info':
24
+ return '#0D96F2';
25
+ case 'warn':
26
+ return '#FCA130';
27
+ case 'error':
28
+ return '#F93E3E';
29
+ default:
30
+ return '#666666';
31
+ }
32
+ };
20
33
  export const DebugImgs = {
21
34
  iconLink: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVGhD7ZgxDoMwDEV9/xvQtReoGBASYmFhYmNiYmNhYWJiQQIJqUh8y05wlAYw+T/pD5Dg9xwnhPR6vV6v1+v1er3eGvuOb7yIhxEQvjAC4hRGQJzGCIhLGAFxGSMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIWxgBcQsjIG5hBMQtjIC4hREQtzAC4hZGQNzCCIhbGAFxCyMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIW/+PYBgfhPtL9WBrfKUAAAAASUVORK5CYII='
22
35
  };