radar-sdk-js 5.1.0-beta.0 → 5.1.0-beta.1

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/README.md CHANGED
@@ -55,7 +55,7 @@ Radar.initialize({ publishableKey: 'prj_test_pk_...' });
55
55
  Add the following to your HTML:
56
56
 
57
57
  ```html
58
- <script src="https://js.radar.com/v5.1.0-beta.0/radar.min.js"></script>
58
+ <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
59
59
 
60
60
  <script>
61
61
  Radar.initialize({ publishableKey: 'prj_test_pk_...' });
@@ -134,7 +134,7 @@ the core SDK first, then any plugins you need:
134
134
  <link href="https://js.radar.com/maps/v1.0.0/radar-maps.css" rel="stylesheet" />
135
135
  <link href="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.css" rel="stylesheet" />
136
136
 
137
- <script src="https://js.radar.com/v5.1.0-beta.0/radar.min.js"></script>
137
+ <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
138
138
  <script src="https://js.radar.com/maps/v1.0.0/radar-maps.min.js"></script>
139
139
  <script src="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.min.js"></script>
140
140
  <script src="https://js.radar.com/fraud/v1.0.0/radar-fraud.min.js"></script>
@@ -151,7 +151,7 @@ by ID or element reference.
151
151
  <html>
152
152
  <head>
153
153
  <link href="https://js.radar.com/maps/v1.0.0/radar-maps.css" rel="stylesheet" />
154
- <script src="https://js.radar.com/v5.1.0-beta.0/radar.min.js"></script>
154
+ <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
155
155
  <script src="https://js.radar.com/maps/v1.0.0/radar-maps.min.js"></script>
156
156
  </head>
157
157
 
@@ -178,7 +178,7 @@ by ID or element reference.
178
178
  <html>
179
179
  <head>
180
180
  <link href="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.css" rel="stylesheet" />
181
- <script src="https://js.radar.com/v5.1.0-beta.0/radar.min.js"></script>
181
+ <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
182
182
  <script src="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.min.js"></script>
183
183
  </head>
184
184
 
@@ -211,7 +211,7 @@ are needed for geofencing.
211
211
  ```html
212
212
  <html>
213
213
  <head>
214
- <script src="https://js.radar.com/v5.1.0-beta.0/radar.min.js"></script>
214
+ <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
215
215
  </head>
216
216
 
217
217
  <body>
@@ -0,0 +1,29 @@
1
+ export type DnsProbeResult = {
2
+ status: 'success';
3
+ hostname: string;
4
+ resolver: 'cloudflare-dns.com';
5
+ dnsStatus: number;
6
+ ipv4Answers: string[];
7
+ } | {
8
+ status: 'no_answers';
9
+ hostname: string;
10
+ resolver: 'cloudflare-dns.com';
11
+ dnsStatus: number;
12
+ answerCount: number;
13
+ note: string;
14
+ } | {
15
+ status: 'http_error';
16
+ hostname: string;
17
+ resolver: 'cloudflare-dns.com';
18
+ httpStatus: number;
19
+ } | {
20
+ status: 'fetch_error';
21
+ hostname: string;
22
+ resolver: 'cloudflare-dns.com';
23
+ errorMessage: string;
24
+ };
25
+ /**
26
+ * Runs at most one DoH probe per hostname per page load. Returns a promise you can await
27
+ * from `RadarNetworkError.dnsProbe` — does not block the throwing error path.
28
+ */
29
+ export declare function scheduleDnsOverHttpsProbe(hostname: string | undefined, correlation: Record<string, unknown>): Promise<DnsProbeResult> | null;
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import type { DnsProbeResult } from './dns-over-https';
1
2
  import type { RadarResponse } from './http';
3
+ export type { DnsProbeResult };
2
4
  /** base error class for all Radar SDK errors */
3
5
  export declare abstract class RadarError extends Error {
4
6
  /** legacy status code string (e.g. `'ERROR_PUBLISHABLE_KEY'`) */
@@ -81,11 +83,59 @@ export declare class RadarServerError extends RadarError {
81
83
  readonly response?: RadarResponse;
82
84
  constructor(response?: RadarResponse);
83
85
  }
86
+ /**
87
+ * Diagnostics captured when fetch() rejects before any HTTP response is received.
88
+ * Safe to stringify for logs — does not include request headers or bodies.
89
+ */
90
+ export interface RadarNetworkFailureDetails {
91
+ readonly phase: 'fetch';
92
+ readonly method: string;
93
+ readonly url: string;
94
+ readonly pathname: string;
95
+ readonly search: string;
96
+ /** hostname parsed from the request URL */
97
+ readonly apiHostname?: string;
98
+ /** document origin when in a browser */
99
+ readonly pageOrigin?: string;
100
+ /** document pathname when in a browser */
101
+ readonly pagePath?: string;
102
+ /** `navigator.userAgent` when available */
103
+ readonly userAgent?: string;
104
+ /** true when API hostname differs from the document hostname */
105
+ readonly crossSiteApiCall?: boolean;
106
+ /**
107
+ * Wall-clock ms from immediately before `fetch()` until it rejected (rounded integer).
108
+ * Does not include JSON parse time when a response was received.
109
+ */
110
+ readonly durationMs: number;
111
+ /** whether `navigator.onLine` was false at failure time */
112
+ readonly online: boolean;
113
+ /** true when failure was triggered by AbortController / user abort */
114
+ readonly aborted: boolean;
115
+ readonly errorName: string;
116
+ readonly errorMessage: string;
117
+ /** present when the environment threw an Error with a stack */
118
+ readonly errorStack?: string;
119
+ readonly connectionEffectiveType?: string;
120
+ readonly connectionDownlink?: number;
121
+ readonly connectionRtt?: number;
122
+ readonly connectionSaveData?: boolean;
123
+ readonly requestId?: string;
124
+ }
84
125
  /** thrown when a request times out or the network is unavailable */
85
126
  export declare class RadarNetworkError extends RadarError {
86
127
  readonly name = "RadarNetworkError";
87
128
  readonly status = "ERROR_NETWORK";
88
- constructor();
129
+ /** set when fetch() fails in the SDK HTTP client (omit for API `ERROR_NETWORK` responses) */
130
+ readonly details?: RadarNetworkFailureDetails;
131
+ /** original rejection from fetch; often a `TypeError` or `DOMException` */
132
+ readonly fetchError?: unknown;
133
+ /**
134
+ * Resolves when the optional Cloudflare DNS-over-HTTPS (A record) probe finishes.
135
+ * Only set for non-aborted fetch failures where a probe was scheduled (`null` if skipped in tests, no hostname, or a probe already ran for this host).
136
+ */
137
+ readonly dnsProbe?: Promise<DnsProbeResult>;
138
+ constructor(message?: string, details?: RadarNetworkFailureDetails, fetchError?: unknown, dnsProbe?: Promise<DnsProbeResult> | null);
89
139
  }
90
140
  /** thrown for unexpected/unclassified errors */
91
141
  export declare class RadarUnknownError extends RadarError {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default, default as Radar } from './api';
2
2
  export * as errors from './errors';
3
3
  export type * from './types';
4
+ export type { DnsProbeResult } from './errors';
4
5
  export type { RadarPlugin, RadarPluginContext } from './plugin';
package/dist/logger.d.ts CHANGED
@@ -6,7 +6,7 @@ declare class Logger {
6
6
  static info(message: string): void;
7
7
  /** log a warning-level message */
8
8
  static warn(message: string): void;
9
- /** log an error-level message */
10
- static error(message: string): void;
9
+ /** log an error-level message; optional second argument for structured diagnostics (printed as a separate console.error arg) */
10
+ static error(message: string, data?: unknown): void;
11
11
  }
12
12
  export default Logger;
package/dist/radar.js CHANGED
@@ -1,4 +1,4 @@
1
- var SDK_VERSION = '5.1.0-beta.0';
1
+ var SDK_VERSION = '5.1.0-beta.1';
2
2
 
3
3
  /** global SDK configuration singleton */
4
4
  class Config {
@@ -57,6 +57,167 @@ Config.defaultOptions = {
57
57
  debug: false,
58
58
  };
59
59
 
60
+ const LOG_LEVELS = {
61
+ none: 0,
62
+ error: 1,
63
+ warn: 2,
64
+ info: 3,
65
+ debug: 4,
66
+ };
67
+ // get the numeric level for logLevel option
68
+ const getLevel = () => {
69
+ // disable logging in tests
70
+ if (typeof window !== 'undefined' && window.RADAR_TEST_ENV) {
71
+ return LOG_LEVELS.none;
72
+ }
73
+ const { logLevel } = Config.get();
74
+ return logLevel ? LOG_LEVELS[logLevel] : LOG_LEVELS.error;
75
+ };
76
+ /** leveled console logger controlled by SDK config */
77
+ class Logger {
78
+ /** log a debug-level message (only when debug mode is enabled) */
79
+ static debug(message, options) {
80
+ if (getLevel() === LOG_LEVELS.debug) {
81
+ console.log(`Radar SDK (debug): ${message.trim()}`, options);
82
+ }
83
+ }
84
+ /** log an info-level message */
85
+ static info(message) {
86
+ if (getLevel() >= LOG_LEVELS.info) {
87
+ console.log(`Radar SDK: ${message.trim()}`);
88
+ }
89
+ }
90
+ /** log a warning-level message */
91
+ static warn(message) {
92
+ if (getLevel() >= LOG_LEVELS.warn) {
93
+ console.warn(`Radar SDK: ${message.trim()}`);
94
+ }
95
+ }
96
+ /** log an error-level message; optional second argument for structured diagnostics (printed as a separate console.error arg) */
97
+ static error(message, data) {
98
+ if (getLevel() >= LOG_LEVELS.error) {
99
+ const trimmed = message.trim();
100
+ if (data !== undefined) {
101
+ console.error(`Radar SDK: ${trimmed}`, data);
102
+ }
103
+ else {
104
+ console.error(`Radar SDK: ${trimmed}`);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ const CLOUDFLARE_DOH_JSON = 'https://cloudflare-dns.com/dns-query';
111
+ /** hostname -> already ran a probe this session */
112
+ const dnsProbeScheduledForHostname = new Set();
113
+ function getDnsProbeAbortSignal() {
114
+ if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
115
+ return AbortSignal.timeout(8000);
116
+ }
117
+ return undefined;
118
+ }
119
+ async function dnsLookupAJson(hostname) {
120
+ const query = new URL(CLOUDFLARE_DOH_JSON);
121
+ query.searchParams.set('name', hostname);
122
+ query.searchParams.set('type', 'A');
123
+ const res = await fetch(query.toString(), {
124
+ headers: { Accept: 'application/dns-json' },
125
+ signal: getDnsProbeAbortSignal(),
126
+ });
127
+ if (!res.ok) {
128
+ return { kind: 'http_error', httpStatus: res.status };
129
+ }
130
+ return { kind: 'ok', data: (await res.json()) };
131
+ }
132
+ async function runDnsOverHttpsProbe(hostname, correlation) {
133
+ try {
134
+ const outcome = await dnsLookupAJson(hostname);
135
+ if (outcome.kind === 'http_error') {
136
+ const result = {
137
+ status: 'http_error',
138
+ hostname,
139
+ resolver: 'cloudflare-dns.com',
140
+ httpStatus: outcome.httpStatus,
141
+ };
142
+ Logger.error(`DNS-over-HTTPS HTTP error (${hostname})`, {
143
+ resolver: 'cloudflare-dns.com',
144
+ httpStatus: outcome.httpStatus,
145
+ correlation,
146
+ });
147
+ return result;
148
+ }
149
+ const data = outcome.data;
150
+ const ips = data.Status === 0 && Array.isArray(data.Answer) ? data.Answer.map((a) => a.data).filter(Boolean) : [];
151
+ if (ips.length > 0) {
152
+ const result = {
153
+ status: 'success',
154
+ hostname,
155
+ resolver: 'cloudflare-dns.com',
156
+ dnsStatus: data.Status,
157
+ ipv4Answers: ips,
158
+ };
159
+ Logger.error(`DNS-over-HTTPS (${hostname})`, {
160
+ resolver: 'cloudflare-dns.com',
161
+ dnsStatus: data.Status,
162
+ ipv4Answers: ips,
163
+ correlation,
164
+ });
165
+ return result;
166
+ }
167
+ const note = data.Status !== 0
168
+ ? `Resolver returned DNS status ${String(data.Status)} (non-zero)`
169
+ : 'No IPv4 Answer records returned';
170
+ const result = {
171
+ status: 'no_answers',
172
+ hostname,
173
+ resolver: 'cloudflare-dns.com',
174
+ dnsStatus: data.Status,
175
+ answerCount: Array.isArray(data.Answer) ? data.Answer.length : 0,
176
+ note,
177
+ };
178
+ Logger.error(`DNS-over-HTTPS (${hostname})`, {
179
+ resolver: 'cloudflare-dns.com',
180
+ dnsStatus: data.Status,
181
+ answerCount: result.answerCount,
182
+ correlation,
183
+ note,
184
+ });
185
+ return result;
186
+ }
187
+ catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ const result = {
190
+ status: 'fetch_error',
191
+ hostname,
192
+ resolver: 'cloudflare-dns.com',
193
+ errorMessage: message,
194
+ };
195
+ Logger.error(`DNS-over-HTTPS probe failed (${hostname})`, {
196
+ resolver: 'cloudflare-dns.com',
197
+ errorMessage: message,
198
+ correlation,
199
+ });
200
+ return result;
201
+ }
202
+ }
203
+ /**
204
+ * Runs at most one DoH probe per hostname per page load. Returns a promise you can await
205
+ * from `RadarNetworkError.dnsProbe` — does not block the throwing error path.
206
+ */
207
+ function scheduleDnsOverHttpsProbe(hostname, correlation) {
208
+ if (!hostname || hostname.length === 0) {
209
+ return null;
210
+ }
211
+ if (typeof window !== 'undefined' && window.RADAR_TEST_ENV === true) {
212
+ return null;
213
+ }
214
+ if (dnsProbeScheduledForHostname.has(hostname)) {
215
+ return null;
216
+ }
217
+ dnsProbeScheduledForHostname.add(hostname);
218
+ return runDnsOverHttpsProbe(hostname, correlation);
219
+ }
220
+
60
221
  /** base error class for all Radar SDK errors */
61
222
  class RadarError extends Error {
62
223
  constructor(message) {
@@ -159,10 +320,13 @@ class RadarServerError extends RadarError {
159
320
  }
160
321
  /** thrown when a request times out or the network is unavailable */
161
322
  class RadarNetworkError extends RadarError {
162
- constructor() {
163
- super('Request timed out.');
323
+ constructor(message, details, fetchError, dnsProbe) {
324
+ super(message ?? 'Request timed out.');
164
325
  this.name = 'RadarNetworkError';
165
326
  this.status = 'ERROR_NETWORK';
327
+ this.details = details;
328
+ this.fetchError = fetchError;
329
+ this.dnsProbe = dnsProbe ?? undefined;
166
330
  }
167
331
  }
168
332
  /** thrown for unexpected/unclassified errors */
@@ -192,50 +356,6 @@ var errors = /*#__PURE__*/Object.freeze({
192
356
  RadarUnknownError: RadarUnknownError
193
357
  });
194
358
 
195
- const LOG_LEVELS = {
196
- none: 0,
197
- error: 1,
198
- warn: 2,
199
- info: 3,
200
- debug: 4,
201
- };
202
- // get the numeric level for logLevel option
203
- const getLevel = () => {
204
- // disable logging in tests
205
- if (typeof window !== 'undefined' && window.RADAR_TEST_ENV) {
206
- return LOG_LEVELS.none;
207
- }
208
- const { logLevel } = Config.get();
209
- return logLevel ? LOG_LEVELS[logLevel] : LOG_LEVELS.error;
210
- };
211
- /** leveled console logger controlled by SDK config */
212
- class Logger {
213
- /** log a debug-level message (only when debug mode is enabled) */
214
- static debug(message, options) {
215
- if (getLevel() === LOG_LEVELS.debug) {
216
- console.log(`Radar SDK (debug): ${message.trim()}`, options);
217
- }
218
- }
219
- /** log an info-level message */
220
- static info(message) {
221
- if (getLevel() >= LOG_LEVELS.info) {
222
- console.log(`Radar SDK: ${message.trim()}`);
223
- }
224
- }
225
- /** log a warning-level message */
226
- static warn(message) {
227
- if (getLevel() >= LOG_LEVELS.warn) {
228
- console.warn(`Radar SDK: ${message.trim()}`);
229
- }
230
- }
231
- /** log an error-level message */
232
- static error(message) {
233
- if (getLevel() >= LOG_LEVELS.error) {
234
- console.error(`Radar SDK: ${message.trim()}`);
235
- }
236
- }
237
- }
238
-
239
359
  /** typed localStorage wrapper with `radar-*` namespaced keys */
240
360
  class Storage {
241
361
  /** localStorage key for user ID */
@@ -468,6 +588,108 @@ class Navigator {
468
588
  }
469
589
 
470
590
  const inFlightRequests = new Map();
591
+ /** gather optional Network Information API snapshot (best-effort) */
592
+ function getConnectionSnapshot(nav) {
593
+ if (!nav) {
594
+ return {};
595
+ }
596
+ const c = nav.connection;
597
+ if (!c) {
598
+ return {};
599
+ }
600
+ return {
601
+ ...(c.effectiveType !== undefined ? { connectionEffectiveType: String(c.effectiveType) } : {}),
602
+ ...(typeof c.downlink === 'number' ? { connectionDownlink: c.downlink } : {}),
603
+ ...(typeof c.rtt === 'number' ? { connectionRtt: c.rtt } : {}),
604
+ ...(typeof c.saveData === 'boolean' ? { connectionSaveData: c.saveData } : {}),
605
+ };
606
+ }
607
+ function isAbortError(err, signal) {
608
+ if (signal.aborted) {
609
+ return true;
610
+ }
611
+ if (err instanceof DOMException && err.name === 'AbortError') {
612
+ return true;
613
+ }
614
+ return err instanceof Error && err.name === 'AbortError';
615
+ }
616
+ function parseThrowable(err) {
617
+ if (err instanceof Error) {
618
+ return { name: err.name, message: err.message, stack: err.stack };
619
+ }
620
+ if (typeof err === 'string') {
621
+ return { name: 'string', message: err };
622
+ }
623
+ return { name: 'non_error', message: String(err) };
624
+ }
625
+ /** monotonic-ish timestamp in ms (`performance.now` when available; else `Date.now`) */
626
+ function getHighResTime() {
627
+ return typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
628
+ }
629
+ function buildFetchFailureDetails(method, url, requestId, err, signal,
630
+ /** elapsed ms from immediately before `fetch()` until rejection */
631
+ durationMs) {
632
+ let pathname = '';
633
+ let search = '';
634
+ let apiHostname;
635
+ try {
636
+ const parsed = new URL(url);
637
+ pathname = parsed.pathname;
638
+ search = parsed.search;
639
+ apiHostname = parsed.hostname;
640
+ }
641
+ catch {
642
+ pathname = url;
643
+ }
644
+ const { name, message, stack } = parseThrowable(err);
645
+ const aborted = isAbortError(err, signal);
646
+ const nav = typeof navigator !== 'undefined' ? navigator : undefined;
647
+ let pageOrigin;
648
+ let pagePath;
649
+ let userAgent;
650
+ let crossSiteApiCall;
651
+ if (typeof window !== 'undefined' && window.location && nav) {
652
+ pageOrigin = window.location.origin;
653
+ pagePath = window.location.pathname === '' ? '/' : window.location.pathname;
654
+ userAgent = nav.userAgent;
655
+ if (apiHostname !== undefined && window.location.hostname !== '') {
656
+ crossSiteApiCall = window.location.hostname !== apiHostname;
657
+ }
658
+ }
659
+ const online = Boolean(nav && nav.onLine);
660
+ return {
661
+ phase: 'fetch',
662
+ method,
663
+ url,
664
+ pathname,
665
+ search,
666
+ durationMs: Math.max(0, Math.round(durationMs)),
667
+ ...(apiHostname !== undefined ? { apiHostname } : {}),
668
+ ...(pageOrigin !== undefined ? { pageOrigin } : {}),
669
+ ...(pagePath !== undefined ? { pagePath } : {}),
670
+ ...(userAgent !== undefined ? { userAgent } : {}),
671
+ ...(crossSiteApiCall !== undefined ? { crossSiteApiCall } : {}),
672
+ online,
673
+ aborted,
674
+ errorName: name,
675
+ errorMessage: message,
676
+ ...(stack ? { errorStack: stack } : {}),
677
+ ...getConnectionSnapshot(nav),
678
+ ...(requestId ? { requestId } : {}),
679
+ };
680
+ }
681
+ function fetchFailureUserMessage(details) {
682
+ if (details.aborted) {
683
+ return 'Request aborted.';
684
+ }
685
+ if (details.errorMessage) {
686
+ return details.errorMessage;
687
+ }
688
+ if (!details.online) {
689
+ return 'Network unavailable (browser offline).';
690
+ }
691
+ return 'Network request failed.';
692
+ }
471
693
  /** fetch-based HTTP client for Radar API requests */
472
694
  class Http {
473
695
  /**
@@ -514,6 +736,7 @@ class Http {
514
736
  ...headers,
515
737
  };
516
738
  let response;
739
+ const fetchStartedAt = getHighResTime();
517
740
  try {
518
741
  response = await fetch(url, {
519
742
  method,
@@ -522,19 +745,35 @@ class Http {
522
745
  signal: abortController.signal,
523
746
  });
524
747
  }
525
- catch {
748
+ catch (fetchErr) {
749
+ let dnsProbePromise = null;
750
+ const durationMs = getHighResTime() - fetchStartedAt;
526
751
  // Delete abort controller instance for this request ID if it hasn't yet been replaced with a different one
527
752
  if (requestId && inFlightRequests.get(requestId) === abortController) {
528
753
  inFlightRequests.delete(requestId);
529
754
  }
530
- if (host) {
531
- for (const [pattern, handler] of Http.errorInterceptors) {
532
- if (host.includes(pattern)) {
533
- throw handler(!!Navigator.online());
534
- }
755
+ const fetchFailureDetails = buildFetchFailureDetails(method, url, requestId, fetchErr, abortController.signal, durationMs);
756
+ Logger.error(`Fetch failed (${fetchFailureDetails.aborted ? 'aborted' : fetchFailureDetails.errorName})`, fetchFailureDetails);
757
+ if (!fetchFailureDetails.aborted) {
758
+ dnsProbePromise = scheduleDnsOverHttpsProbe(fetchFailureDetails.apiHostname, {
759
+ requestFailure: {
760
+ method: fetchFailureDetails.method,
761
+ url: fetchFailureDetails.url,
762
+ durationMs: fetchFailureDetails.durationMs,
763
+ phase: fetchFailureDetails.phase,
764
+ errorName: fetchFailureDetails.errorName,
765
+ errorMessage: fetchFailureDetails.errorMessage,
766
+ crossSiteApiCall: fetchFailureDetails.crossSiteApiCall,
767
+ },
768
+ });
769
+ }
770
+ for (const [pattern, interceptorHandler] of Http.errorInterceptors) {
771
+ if ((urlHost ?? '').includes(pattern)) {
772
+ throw interceptorHandler(!!Navigator.online());
535
773
  }
536
774
  }
537
- throw new RadarNetworkError();
775
+ const userMsg = fetchFailureUserMessage(fetchFailureDetails);
776
+ throw new RadarNetworkError(userMsg, fetchFailureDetails, fetchErr, dnsProbePromise);
538
777
  }
539
778
  if (requestId && inFlightRequests.get(requestId) === abortController) {
540
779
  inFlightRequests.delete(requestId);
@@ -569,6 +808,12 @@ class Http {
569
808
  throw new RadarLocationError('Could not determine location.');
570
809
  }
571
810
  else if (error === 'ERROR_NETWORK') {
811
+ Logger.error('Radar API returned ERROR_NETWORK in response meta', {
812
+ httpStatus: response.status,
813
+ method,
814
+ url,
815
+ meta: parsed.meta ?? null,
816
+ });
572
817
  throw new RadarNetworkError();
573
818
  }
574
819
  }