capacitor-gleap-plugin 18.0.0 → 19.0.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.
Files changed (43) hide show
  1. package/CapacitorGleapPlugin.podspec +7 -2
  2. package/Package.swift +31 -0
  3. package/README.md +307 -761
  4. package/android/build.gradle +1 -1
  5. package/android/src/main/java/io/gleapplugins/capacitor/GleapPlugin.java +228 -37
  6. package/dist/docs.json +323 -3907
  7. package/dist/esm/definitions.d.ts +168 -3
  8. package/dist/esm/definitions.js.map +1 -1
  9. package/dist/esm/index.js +9 -1
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/logs/body.d.ts +58 -0
  12. package/dist/esm/logs/body.js +303 -0
  13. package/dist/esm/logs/body.js.map +1 -0
  14. package/dist/esm/logs/consoleCapture.d.ts +29 -0
  15. package/dist/esm/logs/consoleCapture.js +189 -0
  16. package/dist/esm/logs/consoleCapture.js.map +1 -0
  17. package/dist/esm/logs/format.d.ts +16 -0
  18. package/dist/esm/logs/format.js +394 -0
  19. package/dist/esm/logs/format.js.map +1 -0
  20. package/dist/esm/logs/networkCapture.d.ts +65 -0
  21. package/dist/esm/logs/networkCapture.js +654 -0
  22. package/dist/esm/logs/networkCapture.js.map +1 -0
  23. package/dist/esm/logs/redaction.d.ts +54 -0
  24. package/dist/esm/logs/redaction.js +388 -0
  25. package/dist/esm/logs/redaction.js.map +1 -0
  26. package/dist/esm/logs/webViewLogs.d.ts +70 -0
  27. package/dist/esm/logs/webViewLogs.js +297 -0
  28. package/dist/esm/logs/webViewLogs.js.map +1 -0
  29. package/dist/esm/logs/zone.d.ts +5 -0
  30. package/dist/esm/logs/zone.js +15 -0
  31. package/dist/esm/logs/zone.js.map +1 -0
  32. package/dist/esm/web.d.ts +47 -2
  33. package/dist/esm/web.js +43 -0
  34. package/dist/esm/web.js.map +1 -1
  35. package/dist/plugin.cjs.js +2263 -1
  36. package/dist/plugin.cjs.js.map +1 -1
  37. package/dist/plugin.js +2263 -1
  38. package/dist/plugin.js.map +1 -1
  39. package/ios/{Plugin → Sources/GleapPlugin}/GleapPlugin.swift +222 -12
  40. package/package.json +27 -6
  41. package/ios/Plugin/GleapPlugin.h +0 -10
  42. package/ios/Plugin/GleapPlugin.m +0 -66
  43. package/ios/Plugin/Info.plist +0 -24
@@ -0,0 +1,654 @@
1
+ import { BINARY_BODY_OMITTED, BODY_NOT_CAPTURED, BODY_PENDING, MAX_BODY_SIZE, STREAMING_BODY_OMITTED, bytesToBodyText, capBodyText, captureRequestBody, classifyContentType, isBlob, isUrlSearchParams, markerForKind, readBlobHead, readStreamHead, stringifyJsonHead, toBytes, } from './body';
2
+ import { DEFAULT_NETWORK_LOG_BLACKLIST } from './redaction';
3
+ import { runOutsideZone } from './zone';
4
+ export const MAX_NETWORK_ENTRIES = 30;
5
+ const now = () => typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
6
+ /**
7
+ * Records fetch and XMLHttpRequest calls made inside the WebView. The hooks never alter or delay the
8
+ * request: they only read what the app passes in and what comes back, and every hook is guarded so
9
+ * a logging failure cannot break the app's networking.
10
+ */
11
+ export class NetworkCapture {
12
+ constructor(win, onChange) {
13
+ this.win = win;
14
+ this.onChange = onChange;
15
+ this.records = [];
16
+ this.nextId = 1;
17
+ this.active = false;
18
+ this.isInstalled = false;
19
+ this.xhrStates = new WeakMap();
20
+ this.xhrsWithListeners = new WeakSet();
21
+ }
22
+ /** Installs the hooks inactive: nothing is recorded until setActive(true). */
23
+ install() {
24
+ if (this.isInstalled) {
25
+ return;
26
+ }
27
+ this.isInstalled = true;
28
+ try {
29
+ this.patchFetch();
30
+ }
31
+ catch (e) {
32
+ // Ignore.
33
+ }
34
+ try {
35
+ this.patchXhr();
36
+ }
37
+ catch (e) {
38
+ // Ignore.
39
+ }
40
+ }
41
+ /** Inactive: the hooks stay in place but only call through, and the buffer is dropped. */
42
+ setActive(active) {
43
+ this.active = active;
44
+ if (!active) {
45
+ this.records = [];
46
+ }
47
+ }
48
+ getSnapshots() {
49
+ return this.records.map((record) => ({
50
+ id: record.id,
51
+ version: record.version,
52
+ entry: record.entry,
53
+ }));
54
+ }
55
+ // ---------------------------------------------------------------- fetch
56
+ patchFetch() {
57
+ const win = this.win;
58
+ const originalFetch = win.fetch;
59
+ if (typeof originalFetch !== 'function') {
60
+ return;
61
+ }
62
+ const capture = this;
63
+ win.fetch = function gleapFetch() {
64
+ // eslint-disable-next-line prefer-rest-params
65
+ const args = arguments;
66
+ let record = null;
67
+ if (capture.active) {
68
+ try {
69
+ record = capture.startFetch(args[0], args[1]);
70
+ }
71
+ catch (e) {
72
+ record = null;
73
+ }
74
+ }
75
+ let result;
76
+ try {
77
+ result = originalFetch.apply(this, args);
78
+ }
79
+ catch (error) {
80
+ if (record) {
81
+ capture.fail(record, error);
82
+ }
83
+ throw error;
84
+ }
85
+ const promise = result;
86
+ if (!record || !promise || typeof promise.then !== 'function') {
87
+ return result;
88
+ }
89
+ const tracked = record;
90
+ return promise.then((response) => {
91
+ capture.completeFetch(tracked, response);
92
+ return response;
93
+ }, (error) => {
94
+ capture.fail(tracked, error);
95
+ throw error;
96
+ });
97
+ };
98
+ }
99
+ startFetch(input, init) {
100
+ const url = this.resolveUrl(isRequest(input) ? input.url : isUrl(input) ? input.href : input);
101
+ if (!this.shouldCapture(url)) {
102
+ return null;
103
+ }
104
+ const request = isRequest(input) ? input : null;
105
+ const options = init && typeof init === 'object' ? init : undefined;
106
+ const method = String((options === null || options === void 0 ? void 0 : options.method) || (request === null || request === void 0 ? void 0 : request.method) || 'GET').toUpperCase();
107
+ const headers = normalizeHeaders((options === null || options === void 0 ? void 0 : options.headers) !== undefined ? options.headers : request ? request.headers : undefined);
108
+ let payload = '';
109
+ if ((options === null || options === void 0 ? void 0 : options.body) !== undefined && (options === null || options === void 0 ? void 0 : options.body) !== null) {
110
+ payload = this.requestPayload(options.body, headers);
111
+ }
112
+ else if (request && method !== 'GET' && method !== 'HEAD') {
113
+ payload = this.requestObjectPayload(request, headers);
114
+ }
115
+ return this.createRecord(method, url, headers, payload);
116
+ }
117
+ /** The body of a Request object: read from a clone (made before fetch consumes it), head only. */
118
+ requestObjectPayload(request, headers) {
119
+ const kind = classifyContentType(headers['content-type']);
120
+ const marker = markerForKind(kind);
121
+ if (marker) {
122
+ return marker;
123
+ }
124
+ if (typeof Request === 'undefined' || !('body' in Request.prototype)) {
125
+ return BODY_NOT_CAPTURED;
126
+ }
127
+ if (request.body === null) {
128
+ return '';
129
+ }
130
+ let copy;
131
+ try {
132
+ copy = request.clone();
133
+ }
134
+ catch (e) {
135
+ return BODY_NOT_CAPTURED;
136
+ }
137
+ const stream = copy.body;
138
+ if (!stream || typeof stream.getReader !== 'function') {
139
+ return BODY_NOT_CAPTURED;
140
+ }
141
+ return runOutsideZone(() => readStreamHead(stream).then(({ bytes, complete }) => bytesToBodyText(bytes, kind, complete, null), () => BODY_NOT_CAPTURED));
142
+ }
143
+ completeFetch(record, response) {
144
+ try {
145
+ const headers = normalizeHeaders(response.headers);
146
+ this.update(record, (entry) => {
147
+ entry.duration = Math.round(now() - record.startedAt);
148
+ entry.success = true;
149
+ entry.response = {
150
+ status: response.status,
151
+ statusText: response.statusText || '',
152
+ headers,
153
+ responseText: BODY_PENDING,
154
+ };
155
+ });
156
+ this.setResponseText(record, this.fetchResponseBody(response, headers));
157
+ }
158
+ catch (e) {
159
+ // Ignore.
160
+ }
161
+ }
162
+ /**
163
+ * Streaming and binary responses are never cloned. Text responses are cloned synchronously (before
164
+ * the app sees the response) and only the head of the clone is read, then the clone is cancelled.
165
+ */
166
+ fetchResponseBody(response, headers) {
167
+ const kind = classifyContentType(headers['content-type']);
168
+ const marker = markerForKind(kind);
169
+ if (marker) {
170
+ return marker;
171
+ }
172
+ if (response.type === 'opaque' || response.type === 'opaqueredirect') {
173
+ return BODY_NOT_CAPTURED;
174
+ }
175
+ const body = response.body;
176
+ if (body === null) {
177
+ return '';
178
+ }
179
+ if (!body || typeof body.getReader !== 'function' || typeof response.clone !== 'function') {
180
+ return BODY_NOT_CAPTURED;
181
+ }
182
+ let copy;
183
+ try {
184
+ copy = response.clone();
185
+ }
186
+ catch (e) {
187
+ return BODY_NOT_CAPTURED;
188
+ }
189
+ const stream = copy.body;
190
+ if (!stream) {
191
+ return '';
192
+ }
193
+ const totalBytes = identityContentLength(headers);
194
+ return runOutsideZone(() => readStreamHead(stream).then(({ bytes, complete }) => bytesToBodyText(bytes, kind, complete, complete ? null : totalBytes), () => BODY_NOT_CAPTURED));
195
+ }
196
+ // ------------------------------------------------------------------ XHR
197
+ patchXhr() {
198
+ const win = this.win;
199
+ const Xhr = win.XMLHttpRequest;
200
+ if (typeof Xhr !== 'function' || !Xhr || !Xhr.prototype) {
201
+ return;
202
+ }
203
+ const capacitorXhr = win.CapacitorWebXMLHttpRequest;
204
+ if ((capacitorXhr === null || capacitorXhr === void 0 ? void 0 : capacitorXhr.fullObject) && Xhr !== capacitorXhr.fullObject) {
205
+ // CapacitorHttp replaced the constructor and re-assigns the prototype methods on every
206
+ // construction, so the methods are hooked per instance instead.
207
+ this.wrapXhrConstructor(Xhr);
208
+ return;
209
+ }
210
+ const proto = Xhr.prototype;
211
+ const originals = {
212
+ open: proto.open,
213
+ send: proto.send,
214
+ setRequestHeader: proto.setRequestHeader,
215
+ };
216
+ if (typeof originals.open !== 'function' ||
217
+ typeof originals.send !== 'function' ||
218
+ typeof originals.setRequestHeader !== 'function') {
219
+ return;
220
+ }
221
+ this.hookXhrMethods(proto, (_xhr, name) => originals[name]);
222
+ }
223
+ wrapXhrConstructor(Factory) {
224
+ const capture = this;
225
+ const Wrapped = function GleapXMLHttpRequest() {
226
+ const xhr = new Factory();
227
+ try {
228
+ capture.hookXhrMethods(xhr, (instance, name) => Object.getPrototypeOf(instance)[name]);
229
+ }
230
+ catch (e) {
231
+ // Ignore.
232
+ }
233
+ return xhr;
234
+ };
235
+ try {
236
+ Object.assign(Wrapped, Factory);
237
+ }
238
+ catch (e) {
239
+ // Ignore.
240
+ }
241
+ Wrapped.prototype = Factory.prototype;
242
+ this.win.XMLHttpRequest = Wrapped;
243
+ }
244
+ hookXhrMethods(target, original) {
245
+ const capture = this;
246
+ target.open = function () {
247
+ // eslint-disable-next-line prefer-rest-params
248
+ const args = arguments;
249
+ try {
250
+ capture.onXhrOpen(this, args[0], args[1]);
251
+ }
252
+ catch (e) {
253
+ // Ignore.
254
+ }
255
+ return original(this, 'open').apply(this, args);
256
+ };
257
+ target.setRequestHeader = function () {
258
+ // eslint-disable-next-line prefer-rest-params
259
+ const args = arguments;
260
+ try {
261
+ capture.onXhrHeader(this, args[0], args[1]);
262
+ }
263
+ catch (e) {
264
+ // Ignore.
265
+ }
266
+ return original(this, 'setRequestHeader').apply(this, args);
267
+ };
268
+ target.send = function () {
269
+ // eslint-disable-next-line prefer-rest-params
270
+ const args = arguments;
271
+ let record = null;
272
+ try {
273
+ record = capture.onXhrSend(this, args[0]);
274
+ }
275
+ catch (e) {
276
+ record = null;
277
+ }
278
+ try {
279
+ return original(this, 'send').apply(this, args);
280
+ }
281
+ catch (error) {
282
+ if (record) {
283
+ capture.fail(record, error);
284
+ const state = capture.xhrStates.get(this);
285
+ if (state && state.record === record) {
286
+ state.record = null;
287
+ }
288
+ }
289
+ throw error;
290
+ }
291
+ };
292
+ }
293
+ onXhrOpen(xhr, method, url) {
294
+ this.xhrStates.set(xhr, {
295
+ method: String(method || 'GET').toUpperCase(),
296
+ url: this.resolveUrl(isUrl(url) ? url.href : url),
297
+ headers: {},
298
+ record: null,
299
+ headersAt: null,
300
+ failure: null,
301
+ loaded: false,
302
+ });
303
+ }
304
+ onXhrHeader(xhr, name, value) {
305
+ const state = this.xhrStates.get(xhr);
306
+ if (state && typeof name === 'string') {
307
+ addHeader(state.headers, name, String(value));
308
+ }
309
+ }
310
+ onXhrSend(xhr, body) {
311
+ if (!this.active) {
312
+ return null;
313
+ }
314
+ const state = this.xhrStates.get(xhr);
315
+ if (!state || !this.shouldCapture(state.url)) {
316
+ return null;
317
+ }
318
+ this.listenToXhr(xhr);
319
+ state.failure = null;
320
+ state.loaded = false;
321
+ state.headersAt = null;
322
+ const headers = Object.assign({}, state.headers);
323
+ const payload = state.method === 'GET' || state.method === 'HEAD' ? '' : this.requestPayload(body, headers);
324
+ state.record = this.createRecord(state.method, state.url, headers, payload);
325
+ return state.record;
326
+ }
327
+ listenToXhr(xhr) {
328
+ if (this.xhrsWithListeners.has(xhr)) {
329
+ return;
330
+ }
331
+ this.xhrsWithListeners.add(xhr);
332
+ const state = () => this.xhrStates.get(xhr);
333
+ const guard = (fn) => () => {
334
+ try {
335
+ const current = state();
336
+ if (current === null || current === void 0 ? void 0 : current.record) {
337
+ fn(current);
338
+ }
339
+ }
340
+ catch (e) {
341
+ // Ignore.
342
+ }
343
+ };
344
+ xhr.addEventListener('readystatechange', guard((current) => {
345
+ if (current.headersAt === null && xhr.readyState >= 2) {
346
+ current.headersAt = now();
347
+ }
348
+ }));
349
+ xhr.addEventListener('load', guard((current) => (current.loaded = true)));
350
+ xhr.addEventListener('abort', guard((current) => (current.failure = 'abort')));
351
+ xhr.addEventListener('timeout', guard((current) => (current.failure = 'timeout')));
352
+ xhr.addEventListener('error', guard((current) => (current.failure = 'error')));
353
+ xhr.addEventListener('loadend', guard((current) => this.onXhrLoadEnd(xhr, current)));
354
+ }
355
+ onXhrLoadEnd(xhr, state) {
356
+ const record = state.record;
357
+ if (!record) {
358
+ return;
359
+ }
360
+ state.record = null;
361
+ const finishedAt = now();
362
+ if (state.failure || !state.loaded) {
363
+ let errorText = 'Network request failed';
364
+ if (state.failure === 'abort') {
365
+ errorText = 'Request aborted';
366
+ }
367
+ else if (state.failure === 'timeout') {
368
+ errorText = xhr.timeout > 0 ? `Request timed out after ${xhr.timeout} ms` : 'Request timed out';
369
+ }
370
+ this.update(record, (entry) => {
371
+ entry.duration = Math.round(finishedAt - record.startedAt);
372
+ entry.success = false;
373
+ entry.response = { errorText };
374
+ });
375
+ return;
376
+ }
377
+ let rawHeaders = '';
378
+ try {
379
+ rawHeaders = xhr.getAllResponseHeaders();
380
+ }
381
+ catch (e) {
382
+ rawHeaders = '';
383
+ }
384
+ const headers = parseRawHeaders(rawHeaders);
385
+ this.update(record, (entry) => {
386
+ entry.duration = Math.round((state.headersAt !== null ? state.headersAt : finishedAt) - record.startedAt);
387
+ entry.success = true;
388
+ entry.response = {
389
+ status: xhr.status,
390
+ statusText: xhr.statusText || '',
391
+ headers,
392
+ responseText: BODY_PENDING,
393
+ };
394
+ });
395
+ let body;
396
+ try {
397
+ body = this.xhrResponseBody(xhr, headers);
398
+ }
399
+ catch (e) {
400
+ body = BODY_NOT_CAPTURED;
401
+ }
402
+ this.setResponseText(record, body);
403
+ }
404
+ /** The response is already in memory; only a bounded part of it is turned into text. */
405
+ xhrResponseBody(xhr, headers) {
406
+ const kind = classifyContentType(headers['content-type']);
407
+ if (kind === 'streaming') {
408
+ return STREAMING_BODY_OMITTED;
409
+ }
410
+ const responseType = xhr.responseType || '';
411
+ if (kind === 'binary') {
412
+ return BINARY_BODY_OMITTED;
413
+ }
414
+ if (responseType === '' || responseType === 'text') {
415
+ const text = xhr.responseText;
416
+ if (typeof text !== 'string') {
417
+ return text === null || text === undefined ? '' : stringifyJsonHead(text);
418
+ }
419
+ if (kind === 'unknown' && text.indexOf('�') > -1) {
420
+ // The browser had to replace invalid UTF-8: not a text body.
421
+ return BINARY_BODY_OMITTED;
422
+ }
423
+ return capBodyText(text);
424
+ }
425
+ if (responseType === 'json') {
426
+ const value = xhr.response;
427
+ if (value === null || value === undefined) {
428
+ return BODY_NOT_CAPTURED;
429
+ }
430
+ return typeof value === 'string' ? capBodyText(value) : stringifyJsonHead(value);
431
+ }
432
+ if (responseType === 'arraybuffer') {
433
+ const bytes = toBytes(xhr.response);
434
+ if (!bytes) {
435
+ return BODY_NOT_CAPTURED;
436
+ }
437
+ return bytesToBodyText(bytes, kind, bytes.byteLength <= MAX_BODY_SIZE, bytes.byteLength);
438
+ }
439
+ if (responseType === 'blob') {
440
+ const blob = xhr.response;
441
+ if (!isBlob(blob)) {
442
+ return BODY_NOT_CAPTURED;
443
+ }
444
+ return runOutsideZone(() => readBlobHead(blob, kind).catch(() => BODY_NOT_CAPTURED));
445
+ }
446
+ return BODY_NOT_CAPTURED;
447
+ }
448
+ // --------------------------------------------------------------- shared
449
+ requestPayload(body, headers) {
450
+ if (isUrlSearchParams(body) && !hasOwn(headers, 'content-type')) {
451
+ // Sent with this content type by the browser; recorded so form fields can be redacted.
452
+ headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
453
+ }
454
+ const kind = classifyContentType(headers['content-type']);
455
+ return runOutsideZone(() => captureRequestBody(body, kind));
456
+ }
457
+ createRecord(method, url, headers, payload) {
458
+ const record = {
459
+ id: this.nextId++,
460
+ version: 0,
461
+ startedAt: now(),
462
+ entry: {
463
+ date: new Date().toISOString(),
464
+ type: method,
465
+ url,
466
+ request: {
467
+ headers,
468
+ payload: typeof payload === 'string' ? payload : BODY_PENDING,
469
+ },
470
+ },
471
+ };
472
+ if (typeof payload !== 'string') {
473
+ const setPayload = (text) => this.update(record, (entry) => {
474
+ if (entry.request) {
475
+ entry.request.payload = text;
476
+ }
477
+ });
478
+ runOutsideZone(() => payload.then(setPayload, () => setPayload(BODY_NOT_CAPTURED)));
479
+ }
480
+ this.records.push(record);
481
+ if (this.records.length > MAX_NETWORK_ENTRIES) {
482
+ this.records.splice(0, this.records.length - MAX_NETWORK_ENTRIES);
483
+ }
484
+ this.onChange();
485
+ return record;
486
+ }
487
+ setResponseText(record, body) {
488
+ const apply = (text) => this.update(record, (entry) => {
489
+ if (entry.response && entry.response.errorText === undefined) {
490
+ entry.response.responseText = text;
491
+ }
492
+ });
493
+ if (typeof body === 'string') {
494
+ apply(body);
495
+ return;
496
+ }
497
+ runOutsideZone(() => body.then(apply, () => apply(BODY_NOT_CAPTURED)));
498
+ }
499
+ fail(record, error) {
500
+ try {
501
+ this.update(record, (entry) => {
502
+ entry.duration = Math.round(now() - record.startedAt);
503
+ entry.success = false;
504
+ entry.response = { errorText: describeError(error) };
505
+ });
506
+ }
507
+ catch (e) {
508
+ // Ignore.
509
+ }
510
+ }
511
+ update(record, mutate) {
512
+ try {
513
+ mutate(record.entry);
514
+ }
515
+ catch (e) {
516
+ // Ignore.
517
+ }
518
+ record.version++;
519
+ if (this.active && this.records.indexOf(record) > -1) {
520
+ this.onChange();
521
+ }
522
+ }
523
+ resolveUrl(url) {
524
+ const raw = typeof url === 'string' ? url : String(url);
525
+ try {
526
+ const doc = this.win.document;
527
+ const base = (doc === null || doc === void 0 ? void 0 : doc.baseURI) || this.win.location.href;
528
+ return new URL(raw, base).href;
529
+ }
530
+ catch (e) {
531
+ return raw;
532
+ }
533
+ }
534
+ /**
535
+ * Only real network traffic: http(s) URLs that are not Gleap's own and not the app's bundled files
536
+ * served by Capacitor's local server (http(s)://localhost on Android; capacitor:// on iOS).
537
+ */
538
+ shouldCapture(url) {
539
+ const lower = url.toLowerCase();
540
+ if (lower.indexOf('http://') !== 0 && lower.indexOf('https://') !== 0) {
541
+ return false;
542
+ }
543
+ for (const host of DEFAULT_NETWORK_LOG_BLACKLIST) {
544
+ if (lower.indexOf(host) > -1) {
545
+ return false;
546
+ }
547
+ }
548
+ try {
549
+ const location = this.win.location;
550
+ if (location.hostname === 'localhost' && !location.port) {
551
+ return new URL(url).origin !== location.origin;
552
+ }
553
+ }
554
+ catch (e) {
555
+ // Ignore.
556
+ }
557
+ return true;
558
+ }
559
+ }
560
+ function isRequest(value) {
561
+ return typeof Request !== 'undefined' && value instanceof Request;
562
+ }
563
+ function isUrl(value) {
564
+ return typeof URL !== 'undefined' && value instanceof URL;
565
+ }
566
+ function hasOwn(object, key) {
567
+ return Object.prototype.hasOwnProperty.call(object, key);
568
+ }
569
+ function addHeader(headers, name, value) {
570
+ const key = name.trim().toLowerCase();
571
+ if (!key || key === '__proto__') {
572
+ return;
573
+ }
574
+ headers[key] = hasOwn(headers, key) ? `${headers[key]}, ${value}` : value;
575
+ }
576
+ /** Headers | [name, value][] | record -> { lowercased name: value }, repeated names joined with ", ". */
577
+ export function normalizeHeaders(headers) {
578
+ const result = {};
579
+ if (!headers) {
580
+ return result;
581
+ }
582
+ try {
583
+ if (typeof Headers !== 'undefined' && headers instanceof Headers) {
584
+ headers.forEach((value, name) => addHeader(result, name, value));
585
+ return result;
586
+ }
587
+ if (Array.isArray(headers)) {
588
+ for (const pair of headers) {
589
+ if (pair && pair.length >= 2) {
590
+ addHeader(result, String(pair[0]), String(pair[1]));
591
+ }
592
+ }
593
+ return result;
594
+ }
595
+ if (typeof headers === 'object') {
596
+ const record = headers;
597
+ for (const name of Object.keys(record)) {
598
+ const value = record[name];
599
+ if (value !== undefined && value !== null) {
600
+ addHeader(result, name, String(value));
601
+ }
602
+ }
603
+ }
604
+ }
605
+ catch (e) {
606
+ // Ignore.
607
+ }
608
+ return result;
609
+ }
610
+ function parseRawHeaders(raw) {
611
+ const result = {};
612
+ if (typeof raw !== 'string') {
613
+ return result;
614
+ }
615
+ for (const line of raw.split(/\r?\n/)) {
616
+ const index = line.indexOf(':');
617
+ if (index > 0) {
618
+ addHeader(result, line.slice(0, index), line.slice(index + 1).trim());
619
+ }
620
+ }
621
+ return result;
622
+ }
623
+ /** content-length when it describes the decoded body (no content-encoding), else null. */
624
+ function identityContentLength(headers) {
625
+ const encoding = (headers['content-encoding'] || '').trim().toLowerCase();
626
+ if (encoding && encoding !== 'identity') {
627
+ return null;
628
+ }
629
+ const length = parseInt(headers['content-length'] || '', 10);
630
+ return isFinite(length) && length >= 0 ? length : null;
631
+ }
632
+ function describeError(error) {
633
+ try {
634
+ if (error && typeof error === 'object') {
635
+ const { name, message } = error;
636
+ const text = typeof message === 'string' ? message : '';
637
+ if (name === 'AbortError') {
638
+ return text ? `Request aborted (${text})` : 'Request aborted';
639
+ }
640
+ if (name === 'TimeoutError') {
641
+ return text ? `Request timed out (${text})` : 'Request timed out';
642
+ }
643
+ if (text) {
644
+ return typeof name === 'string' && name ? `${name}: ${text}` : text;
645
+ }
646
+ }
647
+ const text = String(error);
648
+ return text && text !== 'undefined' ? text : 'Request failed';
649
+ }
650
+ catch (e) {
651
+ return 'Request failed';
652
+ }
653
+ }
654
+ //# sourceMappingURL=networkCapture.js.map