@aguacerowx/react-native 0.0.50 → 0.0.51

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.
Files changed (47) hide show
  1. package/android/src/main/java/com/aguacerowx/reactnative/WeatherFrameProcessorModule.java +36 -0
  2. package/ios/WeatherFrameProcessorModule.swift +33 -2
  3. package/lib/commonjs/WeatherLayerManager.js +54 -6
  4. package/lib/commonjs/WeatherLayerManager.js.map +1 -1
  5. package/lib/commonjs/aguaceroCoreDebugHooks.js +144 -0
  6. package/lib/commonjs/aguaceroCoreDebugHooks.js.map +1 -0
  7. package/lib/commonjs/aguaceroRnDebug.js +351 -0
  8. package/lib/commonjs/aguaceroRnDebug.js.map +1 -0
  9. package/lib/commonjs/index.js +37 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/nexrad/nexradAndroidController.js +13 -0
  12. package/lib/commonjs/nexrad/nexradAndroidController.js.map +1 -1
  13. package/lib/commonjs/nexrad/nexradDiag.js +7 -1
  14. package/lib/commonjs/nexrad/nexradDiag.js.map +1 -1
  15. package/lib/commonjs/satellite/satelliteAndroidController.js +9 -0
  16. package/lib/commonjs/satellite/satelliteAndroidController.js.map +1 -1
  17. package/lib/module/WeatherLayerManager.js +54 -6
  18. package/lib/module/WeatherLayerManager.js.map +1 -1
  19. package/lib/module/aguaceroCoreDebugHooks.js +136 -0
  20. package/lib/module/aguaceroCoreDebugHooks.js.map +1 -0
  21. package/lib/module/aguaceroRnDebug.js +334 -0
  22. package/lib/module/aguaceroRnDebug.js.map +1 -0
  23. package/lib/module/index.js +1 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/nexrad/nexradAndroidController.js +13 -0
  26. package/lib/module/nexrad/nexradAndroidController.js.map +1 -1
  27. package/lib/module/nexrad/nexradDiag.js +7 -1
  28. package/lib/module/nexrad/nexradDiag.js.map +1 -1
  29. package/lib/module/satellite/satelliteAndroidController.js +9 -0
  30. package/lib/module/satellite/satelliteAndroidController.js.map +1 -1
  31. package/lib/typescript/WeatherLayerManager.d.ts.map +1 -1
  32. package/lib/typescript/aguaceroCoreDebugHooks.d.ts +10 -0
  33. package/lib/typescript/aguaceroCoreDebugHooks.d.ts.map +1 -0
  34. package/lib/typescript/aguaceroRnDebug.d.ts +97 -0
  35. package/lib/typescript/aguaceroRnDebug.d.ts.map +1 -0
  36. package/lib/typescript/index.d.ts +1 -0
  37. package/lib/typescript/nexrad/nexradAndroidController.d.ts.map +1 -1
  38. package/lib/typescript/nexrad/nexradDiag.d.ts.map +1 -1
  39. package/lib/typescript/satellite/satelliteAndroidController.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/WeatherLayerManager.js +78 -21
  42. package/src/aguaceroCoreDebugHooks.js +142 -0
  43. package/src/aguaceroRnDebug.js +328 -0
  44. package/src/index.js +8 -0
  45. package/src/nexrad/nexradAndroidController.js +11 -1
  46. package/src/nexrad/nexradDiag.js +7 -1
  47. package/src/satellite/satelliteAndroidController.js +9 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * React Native SDK auth / HTTP diagnostics.
3
+ *
4
+ * Enable in your app (pick one):
5
+ *
6
+ * 1. **Recommended** — pass `debug={true}` on {@link WeatherLayerManager}:
7
+ * ```jsx
8
+ * <WeatherLayerManager apiKey={key} debug gridRequestSiteOrigin="https://your-allowed-origin.com" />
9
+ * ```
10
+ *
11
+ * 2. **Global** — before rendering weather (works in release builds):
12
+ * ```js
13
+ * import { configureAguaceroRnDebug } from '@aguacerowx/react-native';
14
+ * configureAguaceroRnDebug({ enabled: true });
15
+ * ```
16
+ * or: `globalThis.__AGUACERO_DEBUG__ = true` in your entry file.
17
+ *
18
+ * Logs use the prefix `[AguaceroRN][debug]` (Metro, Xcode, Logcat).
19
+ * API keys are never printed in full — only length, fingerprint, and whitespace hints.
20
+ */
21
+
22
+ import { Platform } from 'react-native';
23
+
24
+ const LOG_PREFIX = '[AguaceroRN][debug]';
25
+
26
+ /** @type {boolean | null} */
27
+ let _explicitEnabled = null;
28
+
29
+ let _fetchLoggerInstalled = false;
30
+ let _fetchSeq = 0;
31
+
32
+ const AGUACERO_URL_MARKERS = [
33
+ 'cloudfront.net',
34
+ 'lambda-url.us-east-2.on.aws',
35
+ 'amazonaws.com',
36
+ 'noaa.gov',
37
+ ];
38
+
39
+ /**
40
+ * @param {boolean} enabled
41
+ */
42
+ export function setAguaceroRnDebugEnabled(enabled) {
43
+ configureAguaceroRnDebug({ enabled: Boolean(enabled) });
44
+ }
45
+
46
+ /**
47
+ * @param {{ enabled?: boolean }} opts
48
+ */
49
+ export function configureAguaceroRnDebug(opts = {}) {
50
+ if (opts && typeof opts.enabled === 'boolean') {
51
+ _explicitEnabled = opts.enabled;
52
+ }
53
+ const on = isAguaceroRnDebugEnabled();
54
+ try {
55
+ if (typeof globalThis !== 'undefined') {
56
+ globalThis.__AGUACERO_DEBUG__ = on;
57
+ globalThis.__AGUACERO_WX_GRID_DEBUG__ = on;
58
+ globalThis.__AGUACERO_NEXRAD_DEBUG__ = on;
59
+ }
60
+ } catch {
61
+ /* ignore */
62
+ }
63
+ if (on) {
64
+ installGlobalFetchLogger();
65
+ aguaceroDebug('debug.enabled', {
66
+ platform: Platform.OS,
67
+ explicitFlag: _explicitEnabled,
68
+ globalFlag: safeGlobalDebugFlag(),
69
+ });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * True when `debug={true}`, {@link configureAguaceroRnDebug}, or `globalThis.__AGUACERO_DEBUG__ === true`.
75
+ * @returns {boolean}
76
+ */
77
+ export function isAguaceroRnDebugEnabled() {
78
+ if (_explicitEnabled === true) return true;
79
+ if (_explicitEnabled === false) {
80
+ try {
81
+ return typeof globalThis !== 'undefined' && globalThis.__AGUACERO_DEBUG__ === true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+ try {
87
+ if (typeof __DEV__ !== 'undefined' && __DEV__) return false;
88
+ } catch {
89
+ /* ignore */
90
+ }
91
+ try {
92
+ return typeof globalThis !== 'undefined' && globalThis.__AGUACERO_DEBUG__ === true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function safeGlobalDebugFlag() {
99
+ try {
100
+ return typeof globalThis !== 'undefined' ? globalThis.__AGUACERO_DEBUG__ === true : false;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {string | null | undefined} secret
108
+ * @returns {Record<string, unknown>}
109
+ */
110
+ export function describeSecret(secret) {
111
+ if (secret == null || secret === '') {
112
+ return { present: false, length: 0 };
113
+ }
114
+ const s = String(secret);
115
+ const trimmed = s.trim();
116
+ return {
117
+ present: true,
118
+ length: s.length,
119
+ trimmedLength: trimmed.length,
120
+ hasLeadingWhitespace: s.length > 0 && s !== trimmed && /^\s/.test(s),
121
+ hasTrailingWhitespace: s.length > 0 && s !== trimmed && /\s$/.test(s),
122
+ hasInternalWhitespace: /\s/.test(trimmed) && trimmed.indexOf(' ') >= 0,
123
+ fingerprint: fingerprintSecret(trimmed || s),
124
+ looksLikePlaceholder: /^(your[-_]?)?api[-_]?key|xxx+|test+$/i.test(trimmed),
125
+ };
126
+ }
127
+
128
+ /**
129
+ * @param {string} s
130
+ * @returns {string}
131
+ */
132
+ export function fingerprintSecret(s) {
133
+ if (!s) return '(empty)';
134
+ if (s.length <= 8) return `len${s.length}`;
135
+ return `${s.slice(0, 4)}…${s.slice(-4)} (len=${s.length})`;
136
+ }
137
+
138
+ /**
139
+ * @param {string} u
140
+ * @returns {string}
141
+ */
142
+ export function redactApiKeyFromUrl(u) {
143
+ if (!u || typeof u !== 'string') return String(u);
144
+ return u.replace(/([?&])apiKey=[^&]*/gi, '$1apiKey=(redacted)');
145
+ }
146
+
147
+ /**
148
+ * @param {string} url
149
+ * @returns {boolean}
150
+ */
151
+ export function shouldLogAguaceroUrl(url) {
152
+ if (!url || typeof url !== 'string') return false;
153
+ const lower = url.toLowerCase();
154
+ return AGUACERO_URL_MARKERS.some((m) => lower.includes(m));
155
+ }
156
+
157
+ /**
158
+ * @param {string} tag
159
+ * @param {Record<string, unknown> | undefined} detail
160
+ */
161
+ export function aguaceroDebug(tag, detail) {
162
+ if (!isAguaceroRnDebugEnabled()) return;
163
+ if (detail !== undefined) {
164
+ console.warn(`${LOG_PREFIX}[${tag}]`, detail);
165
+ } else {
166
+ console.warn(`${LOG_PREFIX}[${tag}]`);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Auth / HTTP failures: logged when debug is on (same prefix).
172
+ * @param {string} tag
173
+ * @param {Record<string, unknown> | undefined} detail
174
+ */
175
+ export function aguaceroDebugWarn(tag, detail) {
176
+ if (!isAguaceroRnDebugEnabled()) return;
177
+ if (detail !== undefined) {
178
+ console.warn(`${LOG_PREFIX}[WARN][${tag}]`, detail);
179
+ } else {
180
+ console.warn(`${LOG_PREFIX}[WARN][${tag}]`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @param {import('@aguacerowx/javascript-sdk').AguaceroCore | { apiKey?: string; bundleId?: string | null; gridRequestSiteOrigin?: string | null; baseGridUrl?: string; isReactNative?: boolean }} core
186
+ * @param {Record<string, unknown>} [extra]
187
+ */
188
+ export function getAguaceroAuthDiagnosticSnapshot(core, extra = {}) {
189
+ const apiKey = core?.apiKey;
190
+ const bundleId = core?.bundleId;
191
+ const origin = core?.gridRequestSiteOrigin;
192
+ return {
193
+ platform: Platform.OS,
194
+ isReactNative: Boolean(core?.isReactNative),
195
+ baseGridUrl: core?.baseGridUrl ?? null,
196
+ apiKey: describeSecret(apiKey),
197
+ bundleId: bundleId
198
+ ? { present: true, value: String(bundleId), length: String(bundleId).length }
199
+ : { present: false, hint: 'Install react-native-device-info for x-app-identifier on CDN requests' },
200
+ gridRequestSiteOrigin: origin
201
+ ? { present: true, value: String(origin), length: String(origin).length }
202
+ : {
203
+ present: false,
204
+ hint: 'Pass gridRequestSiteOrigin on WeatherLayerManager — many CloudFront rules require Origin + Referer',
205
+ },
206
+ willSendAppIdentifier: Boolean(bundleId && core?.isReactNative),
207
+ willSendOriginHeaders: Boolean(origin && String(origin).trim()),
208
+ ...extra,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * @param {Record<string, unknown>} options - {@link buildGridFrameProcessOptions} output
214
+ * @param {import('@aguacerowx/javascript-sdk').AguaceroCore} [core]
215
+ * @returns {Record<string, unknown>}
216
+ */
217
+ export function augmentProcessFrameOptionsForDebug(options, core) {
218
+ const out = { ...options };
219
+ if (!isAguaceroRnDebugEnabled()) {
220
+ return out;
221
+ }
222
+ out.debug = true;
223
+ aguaceroDebug('processFrame.options', {
224
+ url: redactApiKeyFromUrl(out.url),
225
+ hasApiKeyInQuery: typeof out.url === 'string' && /[?&]apiKey=/i.test(out.url),
226
+ apiKey: describeSecret(out.apiKey),
227
+ bundleId: out.bundleId ? { present: true, value: String(out.bundleId) } : { present: false },
228
+ gridRequestSiteOrigin: out.gridRequestSiteOrigin ?? null,
229
+ coreSnapshot: core ? getAguaceroAuthDiagnosticSnapshot(core) : undefined,
230
+ });
231
+ return out;
232
+ }
233
+
234
+ /**
235
+ * @param {string | Request} input
236
+ * @param {RequestInit | undefined} init
237
+ */
238
+ function summarizeFetchRequest(input, init) {
239
+ const url = typeof input === 'string' ? input : input?.url;
240
+ const headers = new Headers(
241
+ (typeof input !== 'string' && input?.headers) || init?.headers || undefined,
242
+ );
243
+ const headerRecord = {};
244
+ headers.forEach((v, k) => {
245
+ const lk = k.toLowerCase();
246
+ if (lk === 'x-api-key' || lk === 'authorization') {
247
+ headerRecord[k] = describeSecret(v);
248
+ } else {
249
+ headerRecord[k] = v;
250
+ }
251
+ });
252
+ return {
253
+ url: redactApiKeyFromUrl(url || ''),
254
+ method: init?.method || (typeof input !== 'string' ? input?.method : undefined) || 'GET',
255
+ headers: headerRecord,
256
+ hasApiKeyQuery: typeof url === 'string' && /[?&]apiKey=/i.test(url),
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Patches `global.fetch` once to log Aguacero CDN / API traffic when debug is enabled.
262
+ */
263
+ export function installGlobalFetchLogger() {
264
+ if (_fetchLoggerInstalled || !isAguaceroRnDebugEnabled()) return;
265
+ if (typeof globalThis === 'undefined' || typeof globalThis.fetch !== 'function') return;
266
+
267
+ const originalFetch = globalThis.fetch.bind(globalThis);
268
+ _fetchLoggerInstalled = true;
269
+
270
+ globalThis.fetch = async function aguaceroInstrumentedFetch(input, init) {
271
+ const url = typeof input === 'string' ? input : input?.url;
272
+ const shouldLog = shouldLogAguaceroUrl(url || '');
273
+ const reqId = shouldLog ? `f${++_fetchSeq}` : null;
274
+ const started = Date.now();
275
+
276
+ if (shouldLog) {
277
+ aguaceroDebug('fetch.start', { reqId, ...summarizeFetchRequest(input, init) });
278
+ }
279
+
280
+ try {
281
+ const response = await originalFetch(input, init);
282
+ if (shouldLog) {
283
+ const elapsedMs = Date.now() - started;
284
+ const base = {
285
+ reqId,
286
+ status: response.status,
287
+ statusText: response.statusText,
288
+ ok: response.ok,
289
+ elapsedMs,
290
+ url: redactApiKeyFromUrl(url || ''),
291
+ };
292
+ if (!response.ok) {
293
+ let bodySnippet = null;
294
+ try {
295
+ const clone = response.clone();
296
+ const text = await clone.text();
297
+ bodySnippet = text.length > 600 ? `${text.slice(0, 600)}…` : text;
298
+ } catch {
299
+ bodySnippet = '(could not read body)';
300
+ }
301
+ aguaceroDebugWarn('fetch.httpError', {
302
+ ...base,
303
+ bodySnippet,
304
+ hint403:
305
+ response.status === 403
306
+ ? '403 usually means: invalid/disabled API key, bundleId not allowlisted (x-app-identifier), or missing/wrong gridRequestSiteOrigin (Origin/Referer). Compare snapshot at core.created / processFrame.options.'
307
+ : undefined,
308
+ });
309
+ } else {
310
+ aguaceroDebug('fetch.ok', base);
311
+ }
312
+ }
313
+ return response;
314
+ } catch (err) {
315
+ if (shouldLog) {
316
+ aguaceroDebugWarn('fetch.throw', {
317
+ reqId,
318
+ message: err?.message || String(err),
319
+ elapsedMs: Date.now() - started,
320
+ url: redactApiKeyFromUrl(url || ''),
321
+ });
322
+ }
323
+ throw err;
324
+ }
325
+ };
326
+
327
+ aguaceroDebug('fetch.hookInstalled', { platform: Platform.OS });
328
+ }
package/src/index.js CHANGED
@@ -1,5 +1,13 @@
1
1
  export { MapManager } from './MapManager';
2
2
  export { WeatherLayerManager } from './WeatherLayerManager';
3
+ export {
4
+ configureAguaceroRnDebug,
5
+ setAguaceroRnDebugEnabled,
6
+ isAguaceroRnDebugEnabled,
7
+ getAguaceroAuthDiagnosticSnapshot,
8
+ aguaceroDebug,
9
+ aguaceroDebugWarn,
10
+ } from './aguaceroRnDebug';
3
11
  export { default as GridRenderLayer } from './GridRenderLayerNativeComponent';
4
12
  export {
5
13
  AGUACERO_NEXRAD_MAP_LAYER_ID,
@@ -18,6 +18,7 @@ import { sampleNexradFrameAtLatLon } from './nexradCrossSectionSampleAtLatLon.bu
18
18
  import { buildNexradLutRgba } from './nexradLutBuild.js';
19
19
  import { Platform } from 'react-native';
20
20
  import { nexradDiagBootSnapshot, nexradDiagGateTextureSummary, nexradPerfSpan } from './nexradDiag.js';
21
+ import { aguaceroDebug, getAguaceroAuthDiagnosticSnapshot, isAguaceroRnDebugEnabled } from '../aguaceroRnDebug';
21
22
 
22
23
  nexradDiagBootSnapshot({
23
24
  archiveExports: {
@@ -258,6 +259,9 @@ export class NexradAndroidController {
258
259
  setNexradSitesFetchAuth(core.apiKey || '', core.bundleId || '');
259
260
  }
260
261
  setNexradArchiveSiteOrigin(core.gridRequestSiteOrigin || '');
262
+ if (isAguaceroRnDebugEnabled()) {
263
+ aguaceroDebug('nexrad.authConfigured', getAguaceroAuthDiagnosticSnapshot(core, { phase: 'NexradAndroidController.constructor' }));
264
+ }
261
265
  }
262
266
 
263
267
  _trimNativeGpuReadyKeys(max) {
@@ -739,7 +743,13 @@ export class NexradAndroidController {
739
743
  setNexradArchiveBundleId(this.core.bundleId || '');
740
744
  setNexradArchiveSiteOrigin(this.core.gridRequestSiteOrigin || '');
741
745
  setNexradSitesFetchAuth(this.core.apiKey || '', this.core.bundleId || '');
742
-
746
+ if (isAguaceroRnDebugEnabled()) {
747
+ aguaceroDebug('nexrad.authConfigured', getAguaceroAuthDiagnosticSnapshot(this.core, {
748
+ phase: 'NexradAndroidController.preload',
749
+ site: state.nexradSite,
750
+ product: state.nexradProduct,
751
+ }));
752
+ }
743
753
 
744
754
  const snapshot = { ...state };
745
755
  const preloadBatch = nexradPerfSpan(`preload.batch site=${state.nexradSite} times=${times.length}`);
@@ -8,7 +8,13 @@
8
8
  */
9
9
 
10
10
  export function nexradDiagEnabled() {
11
- return typeof globalThis !== 'undefined' && globalThis.__AGUACERO_NEXRAD_DEBUG === true;
11
+ if (typeof globalThis !== 'undefined' && globalThis.__AGUACERO_NEXRAD_DEBUG === true) {
12
+ return true;
13
+ }
14
+ if (typeof globalThis !== 'undefined' && globalThis.__AGUACERO_DEBUG__ === true) {
15
+ return true;
16
+ }
17
+ return false;
12
18
  }
13
19
 
14
20
  /**
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { resolveSatelliteS3FileName } from '@aguacerowx/javascript-sdk';
6
6
  import { satBridgeWarn } from '../satelliteBridgeDiag';
7
+ import { aguaceroDebug, getAguaceroAuthDiagnosticSnapshot, isAguaceroRnDebugEnabled, redactApiKeyFromUrl } from '../aguaceroRnDebug';
7
8
 
8
9
  /**
9
10
  * Target frame first, then remaining frames by increasing temporal distance to {@code targetUnix}.
@@ -181,6 +182,14 @@ export class SatelliteAndroidController {
181
182
  const timelineChanged = timelineSig !== this._cachedTimelineSig;
182
183
 
183
184
  if (runKeyChanged || timelineChanged) {
185
+ if (isAguaceroRnDebugEnabled()) {
186
+ aguaceroDebug('satellite.sync.payload', {
187
+ auth: getAguaceroAuthDiagnosticSnapshot(this.core),
188
+ frameCount: frames.length,
189
+ sampleFrameUrl: frames[0] ? redactApiKeyFromUrl(frames[0].url) : null,
190
+ gridRequestSiteOrigin: mergedState.gridRequestSiteOrigin ?? this.core.gridRequestSiteOrigin ?? null,
191
+ });
192
+ }
184
193
  const sortedFrames = sortFramesByTargetProximity(frames, targetUnix);
185
194
  const payload = {
186
195
  runKey: satRunKey,