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 +5 -5
- package/dist/dns-over-https.d.ts +29 -0
- package/dist/errors.d.ts +51 -1
- package/dist/index.d.ts +1 -0
- package/dist/logger.d.ts +2 -2
- package/dist/radar.js +299 -54
- package/dist/radar.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/dns-over-https.ts +166 -0
- package/src/errors.ts +65 -2
- package/src/http.ts +187 -7
- package/src/index.ts +1 -0
- package/src/logger.ts +8 -3
- package/src/version.ts +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
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.
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
}
|