capacitor-gleap-plugin 18.1.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.
- package/CapacitorGleapPlugin.podspec +7 -2
- package/Package.swift +31 -0
- package/README.md +247 -759
- package/android/build.gradle +1 -1
- package/android/src/main/java/io/gleapplugins/capacitor/GleapPlugin.java +184 -37
- package/dist/docs.json +267 -3893
- package/dist/esm/definitions.d.ts +145 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +9 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logs/body.d.ts +58 -0
- package/dist/esm/logs/body.js +303 -0
- package/dist/esm/logs/body.js.map +1 -0
- package/dist/esm/logs/consoleCapture.d.ts +29 -0
- package/dist/esm/logs/consoleCapture.js +189 -0
- package/dist/esm/logs/consoleCapture.js.map +1 -0
- package/dist/esm/logs/format.d.ts +16 -0
- package/dist/esm/logs/format.js +394 -0
- package/dist/esm/logs/format.js.map +1 -0
- package/dist/esm/logs/networkCapture.d.ts +65 -0
- package/dist/esm/logs/networkCapture.js +654 -0
- package/dist/esm/logs/networkCapture.js.map +1 -0
- package/dist/esm/logs/redaction.d.ts +54 -0
- package/dist/esm/logs/redaction.js +388 -0
- package/dist/esm/logs/redaction.js.map +1 -0
- package/dist/esm/logs/webViewLogs.d.ts +70 -0
- package/dist/esm/logs/webViewLogs.js +297 -0
- package/dist/esm/logs/webViewLogs.js.map +1 -0
- package/dist/esm/logs/zone.d.ts +5 -0
- package/dist/esm/logs/zone.js +15 -0
- package/dist/esm/logs/zone.js.map +1 -0
- package/dist/esm/web.d.ts +37 -2
- package/dist/esm/web.js +34 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +2254 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +2254 -1
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/GleapPlugin}/GleapPlugin.swift +195 -12
- package/package.json +18 -4
- package/ios/Plugin/GleapPlugin.h +0 -10
- package/ios/Plugin/GleapPlugin.m +0 -68
- package/ios/Plugin/Info.plist +0 -24
package/dist/plugin.js
CHANGED
|
@@ -1,9 +1,2228 @@
|
|
|
1
1
|
var capacitorGleap = (function (exports, core, Gleap$1) {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const MAX_LOG_LENGTH = 1000;
|
|
5
|
+
const MAX_ERROR_LOG_LENGTH = 5000;
|
|
6
|
+
const TRUNCATED_SUFFIX = '… [truncated]';
|
|
7
|
+
const MAX_DEPTH = 5;
|
|
8
|
+
const MAX_KEYS = 50;
|
|
9
|
+
const MAX_ITEMS = 50;
|
|
10
|
+
/** Keeps the head of a log line so the result (incl. the suffix) fits max chars. */
|
|
11
|
+
function truncateLog(text, max) {
|
|
12
|
+
if (text.length <= max) {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
let end = Math.max(0, max - TRUNCATED_SUFFIX.length);
|
|
16
|
+
const last = text.charCodeAt(end - 1);
|
|
17
|
+
if (last >= 0xd800 && last <= 0xdbff) {
|
|
18
|
+
end--;
|
|
19
|
+
}
|
|
20
|
+
return text.slice(0, end) + TRUNCATED_SUFFIX;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Formats console arguments like the browser console does (printf-style %s %d %i %f %o %O %c in
|
|
24
|
+
* the first string), with objects as depth-limited JSON. Stops early once the text exceeds the
|
|
25
|
+
* budget, so logging a huge object stays cheap.
|
|
26
|
+
*/
|
|
27
|
+
function formatConsoleArgs(args, budget) {
|
|
28
|
+
if (!args || args.length === 0) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
let out = '';
|
|
32
|
+
let index = 0;
|
|
33
|
+
const first = args[0];
|
|
34
|
+
if (typeof first === 'string' && first.indexOf('%') > -1 && args.length > 1) {
|
|
35
|
+
index = 1;
|
|
36
|
+
for (let i = 0; i < first.length && out.length <= budget; i++) {
|
|
37
|
+
const char = first[i];
|
|
38
|
+
if (char !== '%' || i + 1 >= first.length) {
|
|
39
|
+
out += char;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const specifier = first[i + 1];
|
|
43
|
+
if (specifier === '%') {
|
|
44
|
+
out += '%';
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if ('sdifoOc'.indexOf(specifier) < 0 || index >= args.length) {
|
|
49
|
+
out += char;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const arg = args[index++];
|
|
53
|
+
i++;
|
|
54
|
+
switch (specifier) {
|
|
55
|
+
case 's':
|
|
56
|
+
out += typeof arg === 'string' ? arg : formatValue(arg, budget);
|
|
57
|
+
break;
|
|
58
|
+
case 'd':
|
|
59
|
+
case 'i':
|
|
60
|
+
out += formatInteger(arg);
|
|
61
|
+
break;
|
|
62
|
+
case 'f':
|
|
63
|
+
out += formatFloat(arg);
|
|
64
|
+
break;
|
|
65
|
+
case 'o':
|
|
66
|
+
case 'O':
|
|
67
|
+
out += formatValue(arg, budget);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (; index < args.length && out.length <= budget; index++) {
|
|
73
|
+
const arg = args[index];
|
|
74
|
+
const text = typeof arg === 'string' ? arg : formatValue(arg, budget);
|
|
75
|
+
out = out.length > 0 ? `${out} ${text}` : text;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function formatInteger(value) {
|
|
80
|
+
if (typeof value === 'number') {
|
|
81
|
+
return String(value < 0 ? Math.ceil(value) : Math.floor(value));
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === 'bigint') {
|
|
84
|
+
return String(value);
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === 'string') {
|
|
87
|
+
return String(parseInt(value, 10));
|
|
88
|
+
}
|
|
89
|
+
return 'NaN';
|
|
90
|
+
}
|
|
91
|
+
function formatFloat(value) {
|
|
92
|
+
if (typeof value === 'number') {
|
|
93
|
+
return String(value);
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === 'string') {
|
|
96
|
+
return String(parseFloat(value));
|
|
97
|
+
}
|
|
98
|
+
return 'NaN';
|
|
99
|
+
}
|
|
100
|
+
/** A single value as text: strings as they are, errors with their stack, objects as JSON. */
|
|
101
|
+
function formatValue(value, budget) {
|
|
102
|
+
try {
|
|
103
|
+
if (typeof value === 'string') {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
if (isError(value)) {
|
|
107
|
+
return formatError(value);
|
|
108
|
+
}
|
|
109
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
110
|
+
return formatPrimitive(value);
|
|
111
|
+
}
|
|
112
|
+
const serializer = new Serializer(budget);
|
|
113
|
+
serializer.write(value, 0);
|
|
114
|
+
return serializer.result();
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
return '[unserializable value]';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** "Name: message" followed by the stack (V8 stacks already start with that line). */
|
|
121
|
+
function formatError(error) {
|
|
122
|
+
try {
|
|
123
|
+
const err = error;
|
|
124
|
+
const name = typeof err.name === 'string' && err.name ? err.name : 'Error';
|
|
125
|
+
const message = typeof err.message === 'string' ? err.message : '';
|
|
126
|
+
const head = message ? `${name}: ${message}` : name;
|
|
127
|
+
const stack = typeof err.stack === 'string' ? err.stack.trim() : '';
|
|
128
|
+
if (!stack) {
|
|
129
|
+
return head;
|
|
130
|
+
}
|
|
131
|
+
if (stack.indexOf(head) === 0) {
|
|
132
|
+
return stack;
|
|
133
|
+
}
|
|
134
|
+
return `${head}\n${stack}`;
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
return 'Error';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function isError(value) {
|
|
141
|
+
if (!value || typeof value !== 'object') {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (typeof Error !== 'undefined' && value instanceof Error) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const candidate = value;
|
|
148
|
+
return (typeof candidate.message === 'string' && typeof candidate.stack === 'string' && typeof candidate.name === 'string');
|
|
149
|
+
}
|
|
150
|
+
function formatPrimitive(value) {
|
|
151
|
+
if (typeof value === 'bigint') {
|
|
152
|
+
return `${value}n`;
|
|
153
|
+
}
|
|
154
|
+
if (typeof value === 'symbol') {
|
|
155
|
+
return value.toString();
|
|
156
|
+
}
|
|
157
|
+
return String(value);
|
|
158
|
+
}
|
|
159
|
+
function describeFunction(value) {
|
|
160
|
+
const name = value.name;
|
|
161
|
+
return `[Function: ${typeof name === 'string' && name ? name : 'anonymous'}]`;
|
|
162
|
+
}
|
|
163
|
+
function describeNode(value) {
|
|
164
|
+
if (typeof Node === 'undefined' || !(value instanceof Node)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (typeof Element !== 'undefined' && value instanceof Element) {
|
|
168
|
+
let text = `<${value.tagName.toLowerCase()}`;
|
|
169
|
+
if (value.id) {
|
|
170
|
+
text += `#${value.id}`;
|
|
171
|
+
}
|
|
172
|
+
const className = value.getAttribute('class');
|
|
173
|
+
if (className) {
|
|
174
|
+
text += `.${className.trim().split(/\s+/).join('.')}`;
|
|
175
|
+
}
|
|
176
|
+
return `${text}>`;
|
|
177
|
+
}
|
|
178
|
+
return value.nodeName;
|
|
179
|
+
}
|
|
180
|
+
function constructorName(value) {
|
|
181
|
+
try {
|
|
182
|
+
const proto = Object.getPrototypeOf(value);
|
|
183
|
+
if (!proto) {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
const ctor = proto.constructor;
|
|
187
|
+
const name = ctor && typeof ctor.name === 'string' ? ctor.name : '';
|
|
188
|
+
return name === 'Object' ? '' : name;
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** JSON-like serialiser with depth, key and length limits and circular protection. */
|
|
195
|
+
class Serializer {
|
|
196
|
+
constructor(budget) {
|
|
197
|
+
this.budget = budget;
|
|
198
|
+
this.parts = [];
|
|
199
|
+
this.length = 0;
|
|
200
|
+
this.ancestors = [];
|
|
201
|
+
}
|
|
202
|
+
result() {
|
|
203
|
+
return this.parts.join('');
|
|
204
|
+
}
|
|
205
|
+
isFull() {
|
|
206
|
+
return this.length > this.budget;
|
|
207
|
+
}
|
|
208
|
+
push(text) {
|
|
209
|
+
if (this.isFull()) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.parts.push(text);
|
|
213
|
+
this.length += text.length;
|
|
214
|
+
}
|
|
215
|
+
write(value, depth) {
|
|
216
|
+
if (this.isFull()) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (value === null || value === undefined) {
|
|
220
|
+
this.push(String(value));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
switch (typeof value) {
|
|
224
|
+
case 'string':
|
|
225
|
+
this.push(JSON.stringify(value));
|
|
226
|
+
return;
|
|
227
|
+
case 'number':
|
|
228
|
+
case 'boolean':
|
|
229
|
+
case 'bigint':
|
|
230
|
+
case 'symbol':
|
|
231
|
+
this.push(formatPrimitive(value));
|
|
232
|
+
return;
|
|
233
|
+
case 'function':
|
|
234
|
+
this.push(describeFunction(value));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.writeObject(value, depth);
|
|
238
|
+
}
|
|
239
|
+
writeObject(value, depth) {
|
|
240
|
+
if (this.ancestors.indexOf(value) > -1) {
|
|
241
|
+
this.push('[Circular]');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (isError(value)) {
|
|
245
|
+
this.push(JSON.stringify(formatError(value).split('\n')[0]));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (value instanceof Date) {
|
|
249
|
+
this.push(isNaN(value.getTime()) ? '"Invalid Date"' : JSON.stringify(value.toISOString()));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (value instanceof RegExp) {
|
|
253
|
+
this.push(String(value));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const node = describeNode(value);
|
|
257
|
+
if (node !== null) {
|
|
258
|
+
this.push(node);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (typeof window !== 'undefined' && value === window) {
|
|
262
|
+
this.push('[Window]');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (typeof ArrayBuffer !== 'undefined') {
|
|
266
|
+
if (value instanceof ArrayBuffer) {
|
|
267
|
+
this.push(`[ArrayBuffer(${value.byteLength})]`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (ArrayBuffer.isView(value)) {
|
|
271
|
+
const length = value.length;
|
|
272
|
+
this.push(`[${constructorName(value) || 'ArrayBufferView'}(${typeof length === 'number' ? length : value.byteLength})]`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (typeof Promise !== 'undefined' && value instanceof Promise) {
|
|
277
|
+
this.push('[Promise]');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const isArray = Array.isArray(value);
|
|
281
|
+
if (depth >= MAX_DEPTH) {
|
|
282
|
+
this.push(isArray ? `[Array(${value.length})]` : '[Object]');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
this.ancestors.push(value);
|
|
286
|
+
try {
|
|
287
|
+
if (isArray) {
|
|
288
|
+
this.writeArray(value, depth);
|
|
289
|
+
}
|
|
290
|
+
else if (typeof Map !== 'undefined' && value instanceof Map) {
|
|
291
|
+
this.writeMap(value, depth);
|
|
292
|
+
}
|
|
293
|
+
else if (typeof Set !== 'undefined' && value instanceof Set) {
|
|
294
|
+
this.writeSet(value, depth);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
this.writePlainObject(value, depth);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
this.ancestors.pop();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
writeArray(value, depth) {
|
|
305
|
+
this.push('[');
|
|
306
|
+
const count = Math.min(value.length, MAX_ITEMS);
|
|
307
|
+
for (let i = 0; i < count && !this.isFull(); i++) {
|
|
308
|
+
if (i > 0) {
|
|
309
|
+
this.push(',');
|
|
310
|
+
}
|
|
311
|
+
this.write(value[i], depth + 1);
|
|
312
|
+
}
|
|
313
|
+
if (value.length > MAX_ITEMS) {
|
|
314
|
+
this.push(`,"… ${value.length - MAX_ITEMS} more"`);
|
|
315
|
+
}
|
|
316
|
+
this.push(']');
|
|
317
|
+
}
|
|
318
|
+
writeMap(value, depth) {
|
|
319
|
+
this.push(`Map(${value.size}) {`);
|
|
320
|
+
let count = 0;
|
|
321
|
+
value.forEach((item, key) => {
|
|
322
|
+
if (count >= MAX_ITEMS || this.isFull()) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (count > 0) {
|
|
326
|
+
this.push(', ');
|
|
327
|
+
}
|
|
328
|
+
this.write(key, depth + 1);
|
|
329
|
+
this.push(' => ');
|
|
330
|
+
this.write(item, depth + 1);
|
|
331
|
+
count++;
|
|
332
|
+
});
|
|
333
|
+
this.push('}');
|
|
334
|
+
}
|
|
335
|
+
writeSet(value, depth) {
|
|
336
|
+
this.push(`Set(${value.size}) {`);
|
|
337
|
+
let count = 0;
|
|
338
|
+
value.forEach((item) => {
|
|
339
|
+
if (count >= MAX_ITEMS || this.isFull()) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (count > 0) {
|
|
343
|
+
this.push(', ');
|
|
344
|
+
}
|
|
345
|
+
this.write(item, depth + 1);
|
|
346
|
+
count++;
|
|
347
|
+
});
|
|
348
|
+
this.push('}');
|
|
349
|
+
}
|
|
350
|
+
writePlainObject(value, depth) {
|
|
351
|
+
const record = value;
|
|
352
|
+
if (typeof record.toJSON === 'function') {
|
|
353
|
+
let converted;
|
|
354
|
+
try {
|
|
355
|
+
converted = record.toJSON();
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
converted = undefined;
|
|
359
|
+
}
|
|
360
|
+
if (converted !== undefined && converted !== value) {
|
|
361
|
+
this.write(converted, depth + 1);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const name = constructorName(value);
|
|
366
|
+
if (name) {
|
|
367
|
+
this.push(`${name} `);
|
|
368
|
+
}
|
|
369
|
+
this.push('{');
|
|
370
|
+
const keys = Object.keys(record);
|
|
371
|
+
const count = Math.min(keys.length, MAX_KEYS);
|
|
372
|
+
for (let i = 0; i < count && !this.isFull(); i++) {
|
|
373
|
+
if (i > 0) {
|
|
374
|
+
this.push(',');
|
|
375
|
+
}
|
|
376
|
+
this.push(`${JSON.stringify(keys[i])}:`);
|
|
377
|
+
let child;
|
|
378
|
+
try {
|
|
379
|
+
child = record[keys[i]];
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
child = '[getter threw]';
|
|
383
|
+
}
|
|
384
|
+
this.write(child, depth + 1);
|
|
385
|
+
}
|
|
386
|
+
if (keys.length > MAX_KEYS) {
|
|
387
|
+
this.push(`,"…":"${keys.length - MAX_KEYS} more keys"`);
|
|
388
|
+
}
|
|
389
|
+
this.push('}');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const MAX_CONSOLE_ENTRIES = 500;
|
|
394
|
+
const PATCHED_METHODS = {
|
|
395
|
+
log: 'INFO',
|
|
396
|
+
info: 'INFO',
|
|
397
|
+
debug: 'INFO',
|
|
398
|
+
warn: 'WARNING',
|
|
399
|
+
error: 'ERROR',
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Records the WebView console. Methods are patched in place on the existing console object (never
|
|
403
|
+
* replaced) and always call through to what was there before.
|
|
404
|
+
*/
|
|
405
|
+
class ConsoleCapture {
|
|
406
|
+
constructor(win, onChange) {
|
|
407
|
+
this.win = win;
|
|
408
|
+
this.onChange = onChange;
|
|
409
|
+
this.entries = [];
|
|
410
|
+
this.patched = [];
|
|
411
|
+
this.active = false;
|
|
412
|
+
this.isRecording = false;
|
|
413
|
+
this.errorListener = null;
|
|
414
|
+
this.rejectionListener = null;
|
|
415
|
+
}
|
|
416
|
+
install() {
|
|
417
|
+
if (this.active) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
this.active = true;
|
|
421
|
+
this.patchConsole();
|
|
422
|
+
this.listenForUncaughtErrors();
|
|
423
|
+
}
|
|
424
|
+
/** Restores the console methods that are still ours, stops listening and drops what was recorded. */
|
|
425
|
+
uninstall() {
|
|
426
|
+
this.active = false;
|
|
427
|
+
const console = this.win.console;
|
|
428
|
+
for (const method of this.patched) {
|
|
429
|
+
try {
|
|
430
|
+
if (console[method.name] === method.wrapper) {
|
|
431
|
+
console[method.name] = method.original;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
// The inactive wrapper keeps calling through.
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
this.patched = [];
|
|
439
|
+
if (this.errorListener) {
|
|
440
|
+
this.win.removeEventListener('error', this.errorListener);
|
|
441
|
+
this.errorListener = null;
|
|
442
|
+
}
|
|
443
|
+
if (this.rejectionListener) {
|
|
444
|
+
this.win.removeEventListener('unhandledrejection', this.rejectionListener);
|
|
445
|
+
this.rejectionListener = null;
|
|
446
|
+
}
|
|
447
|
+
this.entries = [];
|
|
448
|
+
}
|
|
449
|
+
getEntries() {
|
|
450
|
+
return this.entries.slice();
|
|
451
|
+
}
|
|
452
|
+
record(priority, text) {
|
|
453
|
+
if (!this.active || !text) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.entries.push({
|
|
457
|
+
date: new Date().toISOString(),
|
|
458
|
+
priority,
|
|
459
|
+
log: truncateLog(text, priority === 'ERROR' ? MAX_ERROR_LOG_LENGTH : MAX_LOG_LENGTH),
|
|
460
|
+
});
|
|
461
|
+
if (this.entries.length > MAX_CONSOLE_ENTRIES) {
|
|
462
|
+
this.entries.splice(0, this.entries.length - MAX_CONSOLE_ENTRIES);
|
|
463
|
+
}
|
|
464
|
+
this.onChange();
|
|
465
|
+
}
|
|
466
|
+
/** Formats and records; re-entrant calls (a getter that logs while being formatted) are dropped. */
|
|
467
|
+
recordArgs(priority, args, prefix = '') {
|
|
468
|
+
if (!this.active || this.isRecording) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.isRecording = true;
|
|
472
|
+
try {
|
|
473
|
+
const budget = priority === 'ERROR' ? MAX_ERROR_LOG_LENGTH : MAX_LOG_LENGTH;
|
|
474
|
+
const text = formatConsoleArgs(args, budget);
|
|
475
|
+
this.record(priority, prefix ? (text ? `${prefix} ${text}` : prefix) : text);
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
// Logging must never break the app.
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
this.isRecording = false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
patchConsole() {
|
|
485
|
+
const console = this.win.console;
|
|
486
|
+
if (!console) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const capture = this;
|
|
490
|
+
for (const name of Object.keys(PATCHED_METHODS)) {
|
|
491
|
+
const priority = PATCHED_METHODS[name];
|
|
492
|
+
this.patchMethod(console, name, function (original, args) {
|
|
493
|
+
capture.recordArgs(priority, args);
|
|
494
|
+
return original.apply(this, args);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
this.patchMethod(console, 'assert', function (original, args) {
|
|
498
|
+
if (!args[0]) {
|
|
499
|
+
capture.recordArgs('ERROR', args.slice(1), 'Assertion failed:');
|
|
500
|
+
}
|
|
501
|
+
return original.apply(this, args);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
patchMethod(console, name, body) {
|
|
505
|
+
const original = console[name];
|
|
506
|
+
if (typeof original !== 'function') {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const target = console;
|
|
510
|
+
const wrapper = function (...args) {
|
|
511
|
+
return body.call(this === undefined ? target : this, original, args);
|
|
512
|
+
};
|
|
513
|
+
try {
|
|
514
|
+
console[name] = wrapper;
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (console[name] === wrapper) {
|
|
520
|
+
this.patched.push({
|
|
521
|
+
name,
|
|
522
|
+
original: original,
|
|
523
|
+
wrapper,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
listenForUncaughtErrors() {
|
|
528
|
+
this.errorListener = (event) => {
|
|
529
|
+
try {
|
|
530
|
+
const errorEvent = event;
|
|
531
|
+
if (typeof ErrorEvent !== 'undefined' && !(event instanceof ErrorEvent) && !errorEvent.error) {
|
|
532
|
+
// Resource load errors are plain Events.
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.recordUncaughtError(errorEvent);
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
// Ignore.
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
this.rejectionListener = (event) => {
|
|
542
|
+
try {
|
|
543
|
+
const reason = event.reason;
|
|
544
|
+
const text = isError(reason) ? formatError(reason) : formatValue(reason, MAX_ERROR_LOG_LENGTH);
|
|
545
|
+
this.recordGuarded(`Unhandled promise rejection: ${text}`);
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
// Ignore.
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
this.win.addEventListener('error', this.errorListener);
|
|
552
|
+
this.win.addEventListener('unhandledrejection', this.rejectionListener);
|
|
553
|
+
}
|
|
554
|
+
recordUncaughtError(event) {
|
|
555
|
+
let text;
|
|
556
|
+
if (event.error && isError(event.error)) {
|
|
557
|
+
text = formatError(event.error);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
text = event.message || formatValue(event.error, MAX_ERROR_LOG_LENGTH);
|
|
561
|
+
if (event.filename) {
|
|
562
|
+
text += ` (${event.filename}:${event.lineno || 0}:${event.colno || 0})`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
this.recordGuarded(text.indexOf('Uncaught') === 0 ? text : `Uncaught ${text}`);
|
|
566
|
+
}
|
|
567
|
+
recordGuarded(text) {
|
|
568
|
+
if (this.isRecording) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.isRecording = true;
|
|
572
|
+
try {
|
|
573
|
+
this.record('ERROR', text);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
this.isRecording = false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Maximum captured size per body (request payload or response text). */
|
|
582
|
+
const MAX_BODY_SIZE = 150000;
|
|
583
|
+
const BINARY_BODY_OMITTED = '[binary body omitted]';
|
|
584
|
+
const STREAMING_BODY_OMITTED = '[streaming body omitted]';
|
|
585
|
+
const BODY_NOT_CAPTURED$1 = '[body not captured]';
|
|
586
|
+
const BODY_PENDING = '[body pending]';
|
|
587
|
+
const STREAMING_TYPES = [
|
|
588
|
+
'text/event-stream',
|
|
589
|
+
'application/x-ndjson',
|
|
590
|
+
'application/stream+json',
|
|
591
|
+
'multipart/x-mixed-replace',
|
|
592
|
+
'grpc',
|
|
593
|
+
];
|
|
594
|
+
const TEXT_TYPES = ['json', 'xml', 'text/', 'javascript', 'x-www-form-urlencoded', 'graphql'];
|
|
595
|
+
function classifyContentType(contentType) {
|
|
596
|
+
if (typeof contentType !== 'string' || contentType.trim() === '') {
|
|
597
|
+
return 'unknown';
|
|
598
|
+
}
|
|
599
|
+
const lower = contentType.toLowerCase();
|
|
600
|
+
for (const type of STREAMING_TYPES) {
|
|
601
|
+
if (lower.indexOf(type) > -1) {
|
|
602
|
+
return 'streaming';
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const type of TEXT_TYPES) {
|
|
606
|
+
if (lower.indexOf(type) > -1) {
|
|
607
|
+
return 'text';
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return 'binary';
|
|
611
|
+
}
|
|
612
|
+
function markerForKind(kind) {
|
|
613
|
+
if (kind === 'streaming') {
|
|
614
|
+
return STREAMING_BODY_OMITTED;
|
|
615
|
+
}
|
|
616
|
+
if (kind === 'binary') {
|
|
617
|
+
return BINARY_BODY_OMITTED;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
/** UTF-8 byte length without allocating an encoded copy. */
|
|
622
|
+
function utf8ByteLength(text) {
|
|
623
|
+
let bytes = 0;
|
|
624
|
+
for (let i = 0; i < text.length; i++) {
|
|
625
|
+
const code = text.charCodeAt(i);
|
|
626
|
+
if (code < 0x80) {
|
|
627
|
+
bytes += 1;
|
|
628
|
+
}
|
|
629
|
+
else if (code < 0x800) {
|
|
630
|
+
bytes += 2;
|
|
631
|
+
}
|
|
632
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
|
|
633
|
+
bytes += 4;
|
|
634
|
+
i++;
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
bytes += 3;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return bytes;
|
|
641
|
+
}
|
|
642
|
+
/** Cuts a string at max chars without splitting a surrogate pair. */
|
|
643
|
+
function sliceHead(text, max) {
|
|
644
|
+
if (text.length <= max) {
|
|
645
|
+
return text;
|
|
646
|
+
}
|
|
647
|
+
let end = max;
|
|
648
|
+
const last = text.charCodeAt(end - 1);
|
|
649
|
+
if (last >= 0xd800 && last <= 0xdbff) {
|
|
650
|
+
end--;
|
|
651
|
+
}
|
|
652
|
+
return text.slice(0, end);
|
|
653
|
+
}
|
|
654
|
+
function truncationMarker(totalBytes) {
|
|
655
|
+
return `\n… [truncated, ${totalBytes !== null && totalBytes > MAX_BODY_SIZE ? totalBytes : `more than ${MAX_BODY_SIZE}`} bytes]`;
|
|
656
|
+
}
|
|
657
|
+
/** Caps a text body at MAX_BODY_SIZE chars: keeps the head and appends the truncation marker. */
|
|
658
|
+
function capBodyText(text) {
|
|
659
|
+
if (text.length <= MAX_BODY_SIZE) {
|
|
660
|
+
return text;
|
|
661
|
+
}
|
|
662
|
+
return sliceHead(text, MAX_BODY_SIZE) + truncationMarker(utf8ByteLength(text));
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Decodes bytes as UTF-8. With fatal (unknown content type) invalid UTF-8 yields null, so the caller
|
|
666
|
+
* can mark the body as binary. A cut-off (incomplete) body keeps a split trailing character pending
|
|
667
|
+
* instead of failing on it.
|
|
668
|
+
*/
|
|
669
|
+
function decodeUtf8(bytes, fatal, complete) {
|
|
670
|
+
if (typeof TextDecoder === 'undefined') {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
return new TextDecoder('utf-8', { fatal }).decode(bytes, {
|
|
675
|
+
stream: !complete,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch (e) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Turns (at most MAX_BODY_SIZE) bytes into the logged text: decoded, capped, marked as truncated
|
|
684
|
+
* when the body was longer, or the binary marker when it is not UTF-8 text.
|
|
685
|
+
*/
|
|
686
|
+
function bytesToBodyText(bytes, kind, complete, totalBytes) {
|
|
687
|
+
const head = bytes.byteLength > MAX_BODY_SIZE ? bytes.subarray(0, MAX_BODY_SIZE) : bytes;
|
|
688
|
+
const cut = !complete || bytes.byteLength > MAX_BODY_SIZE;
|
|
689
|
+
const text = decodeUtf8(head, kind === 'unknown', !cut);
|
|
690
|
+
if (text === null) {
|
|
691
|
+
return kind === 'unknown' ? BINARY_BODY_OMITTED : BODY_NOT_CAPTURED$1;
|
|
692
|
+
}
|
|
693
|
+
return cut ? text + truncationMarker(totalBytes) : text;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Reads at most MAX_BODY_SIZE bytes (plus the rest of the chunk that crossed it) and cancels the
|
|
697
|
+
* stream after that, so an endless or huge body is never read into memory.
|
|
698
|
+
*/
|
|
699
|
+
async function readStreamHead(stream) {
|
|
700
|
+
const reader = stream.getReader();
|
|
701
|
+
const chunks = [];
|
|
702
|
+
let received = 0;
|
|
703
|
+
let complete = false;
|
|
704
|
+
try {
|
|
705
|
+
while (received <= MAX_BODY_SIZE) {
|
|
706
|
+
const result = await reader.read();
|
|
707
|
+
if (result.done) {
|
|
708
|
+
complete = true;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
const chunk = result.value;
|
|
712
|
+
if (chunk && chunk.byteLength > 0) {
|
|
713
|
+
chunks.push(chunk);
|
|
714
|
+
received += chunk.byteLength;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
if (!complete) {
|
|
720
|
+
try {
|
|
721
|
+
reader.cancel().catch(() => undefined);
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
// Ignore.
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const bytes = new Uint8Array(Math.min(received, MAX_BODY_SIZE + 1));
|
|
729
|
+
let offset = 0;
|
|
730
|
+
for (const chunk of chunks) {
|
|
731
|
+
if (offset >= bytes.byteLength) {
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
const part = chunk.subarray(0, bytes.byteLength - offset);
|
|
735
|
+
bytes.set(part, offset);
|
|
736
|
+
offset += part.byteLength;
|
|
737
|
+
}
|
|
738
|
+
return { bytes, complete };
|
|
739
|
+
}
|
|
740
|
+
/** Reads the head of a blob (never the whole blob) and turns it into the logged text. */
|
|
741
|
+
async function readBlobHead(blob, kind) {
|
|
742
|
+
const head = blob.slice(0, MAX_BODY_SIZE);
|
|
743
|
+
if (typeof head.arrayBuffer !== 'function') {
|
|
744
|
+
return BODY_NOT_CAPTURED$1;
|
|
745
|
+
}
|
|
746
|
+
const buffer = await head.arrayBuffer();
|
|
747
|
+
const complete = blob.size <= MAX_BODY_SIZE;
|
|
748
|
+
return bytesToBodyText(new Uint8Array(buffer), kind, complete, complete ? null : blob.size);
|
|
749
|
+
}
|
|
750
|
+
function isBlob(value) {
|
|
751
|
+
return typeof Blob !== 'undefined' && value instanceof Blob;
|
|
752
|
+
}
|
|
753
|
+
function isFormData(value) {
|
|
754
|
+
return typeof FormData !== 'undefined' && value instanceof FormData;
|
|
755
|
+
}
|
|
756
|
+
function isUrlSearchParams(value) {
|
|
757
|
+
return typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams;
|
|
758
|
+
}
|
|
759
|
+
function isReadableStream(value) {
|
|
760
|
+
return typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;
|
|
761
|
+
}
|
|
762
|
+
function isDocument(value) {
|
|
763
|
+
return typeof Document !== 'undefined' && value instanceof Document;
|
|
764
|
+
}
|
|
765
|
+
/** Byte view of an ArrayBuffer or ArrayBufferView, or null for anything else. */
|
|
766
|
+
function toBytes(value) {
|
|
767
|
+
if (typeof ArrayBuffer === 'undefined') {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
if (value instanceof ArrayBuffer) {
|
|
771
|
+
return new Uint8Array(value);
|
|
772
|
+
}
|
|
773
|
+
if (ArrayBuffer.isView(value)) {
|
|
774
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The logged request payload for a fetch/XHR body. Strings and buffers are captured synchronously;
|
|
780
|
+
* blobs are read asynchronously (head only). Form data, streams and documents are not captured.
|
|
781
|
+
*/
|
|
782
|
+
function captureRequestBody(body, kind) {
|
|
783
|
+
if (body === undefined || body === null) {
|
|
784
|
+
return '';
|
|
785
|
+
}
|
|
786
|
+
if (isReadableStream(body)) {
|
|
787
|
+
return STREAMING_BODY_OMITTED;
|
|
788
|
+
}
|
|
789
|
+
const marker = markerForKind(kind);
|
|
790
|
+
if (marker) {
|
|
791
|
+
return marker;
|
|
792
|
+
}
|
|
793
|
+
if (typeof body === 'string') {
|
|
794
|
+
return capBodyText(body);
|
|
795
|
+
}
|
|
796
|
+
if (isUrlSearchParams(body)) {
|
|
797
|
+
return capBodyText(body.toString());
|
|
798
|
+
}
|
|
799
|
+
if (isFormData(body) || isDocument(body)) {
|
|
800
|
+
return BODY_NOT_CAPTURED$1;
|
|
801
|
+
}
|
|
802
|
+
if (isBlob(body)) {
|
|
803
|
+
const blobKind = kind === 'unknown' ? classifyContentType(body.type) : kind;
|
|
804
|
+
const blobMarker = markerForKind(blobKind);
|
|
805
|
+
if (blobMarker) {
|
|
806
|
+
return blobMarker;
|
|
807
|
+
}
|
|
808
|
+
return readBlobHead(body, blobKind).catch(() => BODY_NOT_CAPTURED$1);
|
|
809
|
+
}
|
|
810
|
+
const bytes = toBytes(body);
|
|
811
|
+
if (bytes) {
|
|
812
|
+
return bytesToBodyText(bytes, kind, bytes.byteLength <= MAX_BODY_SIZE, bytes.byteLength);
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
return capBodyText(String(body));
|
|
816
|
+
}
|
|
817
|
+
catch (e) {
|
|
818
|
+
return BODY_NOT_CAPTURED$1;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* JSON.stringify for a parsed JSON value (XHR responseType "json") that stops once the output
|
|
823
|
+
* exceeds MAX_BODY_SIZE, so a huge response is not serialised in full.
|
|
824
|
+
*/
|
|
825
|
+
function stringifyJsonHead(value) {
|
|
826
|
+
const parts = [];
|
|
827
|
+
let length = 0;
|
|
828
|
+
let full = false;
|
|
829
|
+
const push = (text) => {
|
|
830
|
+
if (full) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
parts.push(text);
|
|
834
|
+
length += text.length;
|
|
835
|
+
if (length > MAX_BODY_SIZE) {
|
|
836
|
+
full = true;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const write = (node) => {
|
|
840
|
+
if (full) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (node === null || typeof node !== 'object') {
|
|
844
|
+
const text = JSON.stringify(node);
|
|
845
|
+
push(text === undefined ? 'null' : text);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (Array.isArray(node)) {
|
|
849
|
+
push('[');
|
|
850
|
+
for (let i = 0; i < node.length && !full; i++) {
|
|
851
|
+
if (i > 0) {
|
|
852
|
+
push(',');
|
|
853
|
+
}
|
|
854
|
+
write(node[i]);
|
|
855
|
+
}
|
|
856
|
+
push(']');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
push('{');
|
|
860
|
+
let first = true;
|
|
861
|
+
const record = node;
|
|
862
|
+
for (const key of Object.keys(record)) {
|
|
863
|
+
if (full) {
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
const child = record[key];
|
|
867
|
+
if (child === undefined || typeof child === 'function') {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
push(`${first ? '' : ','}${JSON.stringify(key)}:`);
|
|
871
|
+
first = false;
|
|
872
|
+
write(child);
|
|
873
|
+
}
|
|
874
|
+
push('}');
|
|
875
|
+
};
|
|
876
|
+
write(value);
|
|
877
|
+
const text = parts.join('');
|
|
878
|
+
if (!full) {
|
|
879
|
+
return text;
|
|
880
|
+
}
|
|
881
|
+
return sliceHead(text, MAX_BODY_SIZE) + truncationMarker(null);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** URLs containing one of these are never logged, in addition to the configured blacklist. */
|
|
885
|
+
const DEFAULT_NETWORK_LOG_BLACKLIST = ['gleap.io', 'gleap.ai'];
|
|
886
|
+
const REDACTED_VALUE = '[REDACTED]';
|
|
887
|
+
const BODY_NOT_CAPTURED = '[body not captured]';
|
|
888
|
+
/** Credential headers whose value is always masked (the header itself is kept). */
|
|
889
|
+
const ALWAYS_MASKED_HEADERS = ['authorization', 'proxy-authorization', 'cookie', 'set-cookie'];
|
|
890
|
+
/**
|
|
891
|
+
* Builds the redaction rules from the props to ignore and the blacklist (remote config and local
|
|
892
|
+
* setter calls combined by the caller). Entries are trimmed, lowercased and deduplicated.
|
|
893
|
+
*/
|
|
894
|
+
function createRedactionRules(propsToIgnore, blacklist) {
|
|
895
|
+
const props = new Set();
|
|
896
|
+
const paths = [];
|
|
897
|
+
for (const raw of propsToIgnore || []) {
|
|
898
|
+
if (typeof raw !== 'string') {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const prop = raw.trim().toLowerCase();
|
|
902
|
+
if (!prop || props.has(prop)) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
props.add(prop);
|
|
906
|
+
if (prop.indexOf('.') > -1) {
|
|
907
|
+
const segments = prop.split('.').filter((segment) => segment.length > 0);
|
|
908
|
+
if (segments.length > 1) {
|
|
909
|
+
paths.push(segments);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const blacklistEntries = [];
|
|
914
|
+
for (const raw of [...DEFAULT_NETWORK_LOG_BLACKLIST, ...(blacklist || [])]) {
|
|
915
|
+
if (typeof raw !== 'string') {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const entry = raw.trim().toLowerCase();
|
|
919
|
+
if (entry && blacklistEntries.indexOf(entry) < 0) {
|
|
920
|
+
blacklistEntries.push(entry);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return { props, paths, blacklist: blacklistEntries };
|
|
924
|
+
}
|
|
925
|
+
/** Substring match (case-insensitive) against the blacklist, incl. the default Gleap hosts. */
|
|
926
|
+
function isBlacklistedUrl(url, rules) {
|
|
927
|
+
if (typeof url !== 'string') {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
const lowerUrl = url.toLowerCase();
|
|
931
|
+
for (const entry of rules.blacklist) {
|
|
932
|
+
if (lowerUrl.indexOf(entry) > -1) {
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Removes headers named like a prop (case-insensitive) and masks the credential headers.
|
|
940
|
+
* Returns a new object; the input is not modified.
|
|
941
|
+
*/
|
|
942
|
+
function redactHeaders(headers, rules) {
|
|
943
|
+
if (!headers || typeof headers !== 'object') {
|
|
944
|
+
return headers;
|
|
945
|
+
}
|
|
946
|
+
const result = {};
|
|
947
|
+
for (const name of Object.keys(headers)) {
|
|
948
|
+
const lowerName = name.toLowerCase();
|
|
949
|
+
if (rules.props.has(lowerName)) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
result[name] = ALWAYS_MASKED_HEADERS.indexOf(lowerName) > -1 ? REDACTED_VALUE : headers[name];
|
|
953
|
+
}
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Removes matching keys from a JSON body: every key equal to a prop at any depth (objects inside
|
|
958
|
+
* arrays too), and dotted props additionally as a path from the root, then re-serialises compactly.
|
|
959
|
+
* A body that looks like JSON but does not parse (e.g. cut at the size limit) gets the values of
|
|
960
|
+
* matching keys masked in the text instead. Returns the body untouched when nothing matched.
|
|
961
|
+
*/
|
|
962
|
+
function redactJsonBody(body, rules) {
|
|
963
|
+
if (!body || rules.props.size === 0) {
|
|
964
|
+
return body;
|
|
965
|
+
}
|
|
966
|
+
const firstChar = firstNonWhitespaceChar(body);
|
|
967
|
+
if (firstChar !== '{' && firstChar !== '[') {
|
|
968
|
+
return body;
|
|
969
|
+
}
|
|
970
|
+
let parsed;
|
|
971
|
+
try {
|
|
972
|
+
parsed = JSON.parse(body);
|
|
973
|
+
}
|
|
974
|
+
catch (e) {
|
|
975
|
+
return maskJsonKeysInText(body, rules);
|
|
976
|
+
}
|
|
977
|
+
let changed = removeKeysAtAnyDepth(parsed, rules.props);
|
|
978
|
+
for (const path of rules.paths) {
|
|
979
|
+
if (removePath(parsed, path, 0)) {
|
|
980
|
+
changed = true;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return changed ? JSON.stringify(parsed) : body;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Removes form fields (application/x-www-form-urlencoded) named like a prop. Kept fields keep their
|
|
987
|
+
* original encoding; the body is returned untouched when nothing matched.
|
|
988
|
+
*/
|
|
989
|
+
function redactFormBody(body, rules) {
|
|
990
|
+
if (!body || rules.props.size === 0) {
|
|
991
|
+
return body;
|
|
992
|
+
}
|
|
993
|
+
const filtered = filterParams(body, rules);
|
|
994
|
+
return filtered === null ? body : filtered;
|
|
995
|
+
}
|
|
996
|
+
/** Removes query parameters named like a prop. The URL is returned untouched when nothing matched. */
|
|
997
|
+
function redactUrl(url, rules) {
|
|
998
|
+
if (typeof url !== 'string' || rules.props.size === 0) {
|
|
999
|
+
return url;
|
|
1000
|
+
}
|
|
1001
|
+
const hashIndex = url.indexOf('#');
|
|
1002
|
+
const main = hashIndex > -1 ? url.slice(0, hashIndex) : url;
|
|
1003
|
+
const hash = hashIndex > -1 ? url.slice(hashIndex) : '';
|
|
1004
|
+
const queryIndex = main.indexOf('?');
|
|
1005
|
+
if (queryIndex < 0) {
|
|
1006
|
+
return url;
|
|
1007
|
+
}
|
|
1008
|
+
const filtered = filterParams(main.slice(queryIndex + 1), rules);
|
|
1009
|
+
if (filtered === null) {
|
|
1010
|
+
return url;
|
|
1011
|
+
}
|
|
1012
|
+
return main.slice(0, queryIndex) + (filtered ? `?${filtered}` : '') + hash;
|
|
1013
|
+
}
|
|
1014
|
+
/** Redacts one entry. Returns a new entry; the input is not modified. */
|
|
1015
|
+
function redactNetworkLogEntry(entry, rules) {
|
|
1016
|
+
const result = Object.assign({}, entry);
|
|
1017
|
+
result.url = redactUrl(entry.url, rules);
|
|
1018
|
+
if (entry.request) {
|
|
1019
|
+
const request = Object.assign({}, entry.request);
|
|
1020
|
+
const contentType = getHeader(entry.request.headers, 'content-type');
|
|
1021
|
+
request.headers = redactHeaders(entry.request.headers, rules);
|
|
1022
|
+
if (typeof request.payload === 'string') {
|
|
1023
|
+
request.payload = redactBody(request.payload, contentType, rules);
|
|
1024
|
+
}
|
|
1025
|
+
result.request = request;
|
|
1026
|
+
}
|
|
1027
|
+
if (entry.response) {
|
|
1028
|
+
const response = Object.assign({}, entry.response);
|
|
1029
|
+
const contentType = getHeader(entry.response.headers, 'content-type');
|
|
1030
|
+
if (response.headers) {
|
|
1031
|
+
response.headers = redactHeaders(response.headers, rules);
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof response.responseText === 'string') {
|
|
1034
|
+
response.responseText = redactBody(response.responseText, contentType, rules);
|
|
1035
|
+
}
|
|
1036
|
+
result.response = response;
|
|
1037
|
+
}
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
/** Like redactNetworkLogEntry, but never throws: falls back to an entry without any content. */
|
|
1041
|
+
function redactNetworkLogEntryOrStrip(entry, rules) {
|
|
1042
|
+
try {
|
|
1043
|
+
return redactNetworkLogEntry(entry, rules);
|
|
1044
|
+
}
|
|
1045
|
+
catch (e) {
|
|
1046
|
+
return stripNetworkLogEntry(entry);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/** Keeps method, URL (without query), timing and status; drops headers and bodies. */
|
|
1050
|
+
function stripNetworkLogEntry(entry) {
|
|
1051
|
+
const url = typeof entry.url === 'string' ? entry.url.split(/[?#]/)[0] : '';
|
|
1052
|
+
const stripped = {
|
|
1053
|
+
date: entry.date,
|
|
1054
|
+
type: entry.type,
|
|
1055
|
+
url,
|
|
1056
|
+
duration: entry.duration,
|
|
1057
|
+
success: entry.success,
|
|
1058
|
+
request: { headers: {}, payload: BODY_NOT_CAPTURED },
|
|
1059
|
+
};
|
|
1060
|
+
if (entry.response) {
|
|
1061
|
+
stripped.response =
|
|
1062
|
+
typeof entry.response.errorText === 'string'
|
|
1063
|
+
? { errorText: entry.response.errorText }
|
|
1064
|
+
: {
|
|
1065
|
+
status: entry.response.status,
|
|
1066
|
+
statusText: entry.response.statusText,
|
|
1067
|
+
headers: {},
|
|
1068
|
+
responseText: BODY_NOT_CAPTURED,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
return stripped;
|
|
1072
|
+
}
|
|
1073
|
+
function redactBody(body, contentType, rules) {
|
|
1074
|
+
if (!body || rules.props.size === 0) {
|
|
1075
|
+
return body;
|
|
1076
|
+
}
|
|
1077
|
+
const json = redactJsonBody(body, rules);
|
|
1078
|
+
if (json !== body) {
|
|
1079
|
+
return json;
|
|
1080
|
+
}
|
|
1081
|
+
const lowerContentType = (contentType || '').toLowerCase();
|
|
1082
|
+
if (lowerContentType.indexOf('x-www-form-urlencoded') > -1 || (!lowerContentType && looksLikeFormBody(body))) {
|
|
1083
|
+
return redactFormBody(body, rules);
|
|
1084
|
+
}
|
|
1085
|
+
return body;
|
|
1086
|
+
}
|
|
1087
|
+
function looksLikeFormBody(body) {
|
|
1088
|
+
return /^[^\s=&]+=[^\s&]*(?:&[^\s=&]*(?:=[^\s&]*)?)*$/.test(body);
|
|
1089
|
+
}
|
|
1090
|
+
function getHeader(headers, name) {
|
|
1091
|
+
if (!headers || typeof headers !== 'object') {
|
|
1092
|
+
return undefined;
|
|
1093
|
+
}
|
|
1094
|
+
for (const key of Object.keys(headers)) {
|
|
1095
|
+
if (key.toLowerCase() === name) {
|
|
1096
|
+
return headers[key];
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
function firstNonWhitespaceChar(text) {
|
|
1102
|
+
const match = /\S/.exec(text);
|
|
1103
|
+
return match ? match[0] : '';
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Masks the values of matching keys in JSON text that does not parse: every prop as a whole plus the
|
|
1107
|
+
* last segment of each dotted prop, case-insensitive. A string cut off at the end is masked too;
|
|
1108
|
+
* object and array values are left alone (their inner keys are matched on their own).
|
|
1109
|
+
*/
|
|
1110
|
+
function maskJsonKeysInText(body, rules) {
|
|
1111
|
+
const keys = new Set(rules.props);
|
|
1112
|
+
for (const path of rules.paths) {
|
|
1113
|
+
keys.add(path[path.length - 1]);
|
|
1114
|
+
}
|
|
1115
|
+
let result = body;
|
|
1116
|
+
keys.forEach((key) => {
|
|
1117
|
+
// A JSON string never contains a raw line break, so a string cut at the end stops before the
|
|
1118
|
+
// "\n… [truncated" marker instead of swallowing it.
|
|
1119
|
+
const pattern = new RegExp(`"(${escapeRegExp(key)})"(\\s*:\\s*)("(?:[^"\\\\\\r\\n]|\\\\.)*"?|-?\\d[0-9.eE+-]*|true|false|null)`, 'gi');
|
|
1120
|
+
result = result.replace(pattern, `"$1"$2"${REDACTED_VALUE}"`);
|
|
1121
|
+
});
|
|
1122
|
+
return result === body ? body : result;
|
|
1123
|
+
}
|
|
1124
|
+
function escapeRegExp(text) {
|
|
1125
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1126
|
+
}
|
|
1127
|
+
/** Removes every key equal to a prop at any depth. Iterative, so deep JSON cannot overflow the stack. */
|
|
1128
|
+
function removeKeysAtAnyDepth(root, props) {
|
|
1129
|
+
let changed = false;
|
|
1130
|
+
const stack = [root];
|
|
1131
|
+
while (stack.length > 0) {
|
|
1132
|
+
const node = stack.pop();
|
|
1133
|
+
if (Array.isArray(node)) {
|
|
1134
|
+
for (const item of node) {
|
|
1135
|
+
if (item && typeof item === 'object') {
|
|
1136
|
+
stack.push(item);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
else if (node && typeof node === 'object') {
|
|
1141
|
+
const record = node;
|
|
1142
|
+
for (const key of Object.keys(record)) {
|
|
1143
|
+
if (props.has(key.toLowerCase())) {
|
|
1144
|
+
delete record[key];
|
|
1145
|
+
changed = true;
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
const child = record[key];
|
|
1149
|
+
if (child && typeof child === 'object') {
|
|
1150
|
+
stack.push(child);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return changed;
|
|
1157
|
+
}
|
|
1158
|
+
/** Removes a dotted path from the root; arrays on the way are applied to each element. */
|
|
1159
|
+
function removePath(node, segments, index) {
|
|
1160
|
+
if (Array.isArray(node)) {
|
|
1161
|
+
let changed = false;
|
|
1162
|
+
for (const item of node) {
|
|
1163
|
+
if (removePath(item, segments, index)) {
|
|
1164
|
+
changed = true;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return changed;
|
|
1168
|
+
}
|
|
1169
|
+
if (!node || typeof node !== 'object') {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
const record = node;
|
|
1173
|
+
const segment = segments[index];
|
|
1174
|
+
const isLast = index === segments.length - 1;
|
|
1175
|
+
let changed = false;
|
|
1176
|
+
for (const key of Object.keys(record)) {
|
|
1177
|
+
if (key.toLowerCase() !== segment) {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
if (isLast) {
|
|
1181
|
+
delete record[key];
|
|
1182
|
+
changed = true;
|
|
1183
|
+
}
|
|
1184
|
+
else if (removePath(record[key], segments, index + 1)) {
|
|
1185
|
+
changed = true;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return changed;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Filters an urlencoded parameter list (query string or form body). Returns null when nothing was
|
|
1192
|
+
* removed, so callers can keep the original string.
|
|
1193
|
+
*/
|
|
1194
|
+
function filterParams(params, rules) {
|
|
1195
|
+
const parts = params.split('&');
|
|
1196
|
+
const kept = [];
|
|
1197
|
+
let changed = false;
|
|
1198
|
+
for (const part of parts) {
|
|
1199
|
+
const equalsIndex = part.indexOf('=');
|
|
1200
|
+
const rawName = equalsIndex > -1 ? part.slice(0, equalsIndex) : part;
|
|
1201
|
+
if (rawName && paramNameMatches(decodeParamName(rawName), rules)) {
|
|
1202
|
+
changed = true;
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
kept.push(part);
|
|
1206
|
+
}
|
|
1207
|
+
return changed ? kept.join('&') : null;
|
|
1208
|
+
}
|
|
1209
|
+
function decodeParamName(rawName) {
|
|
1210
|
+
try {
|
|
1211
|
+
return decodeURIComponent(rawName.replace(/\+/g, ' '));
|
|
1212
|
+
}
|
|
1213
|
+
catch (e) {
|
|
1214
|
+
return rawName;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* A param matches when its whole name equals a prop. Bracket names (user[password]) are treated like
|
|
1219
|
+
* nested JSON keys: any segment equal to a prop, or a dotted prop matching the leading segments.
|
|
1220
|
+
*/
|
|
1221
|
+
function paramNameMatches(name, rules) {
|
|
1222
|
+
const lowerName = name.toLowerCase();
|
|
1223
|
+
if (rules.props.has(lowerName)) {
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
if (lowerName.indexOf('[') < 0) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
const segments = lowerName
|
|
1230
|
+
.replace(/\]/g, '')
|
|
1231
|
+
.split('[')
|
|
1232
|
+
.filter((segment) => segment.length > 0);
|
|
1233
|
+
if (segments.length < 2) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
for (const segment of segments) {
|
|
1237
|
+
if (rules.props.has(segment)) {
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
for (const path of rules.paths) {
|
|
1242
|
+
if (path.length <= segments.length) {
|
|
1243
|
+
let matches = true;
|
|
1244
|
+
for (let i = 0; i < path.length; i++) {
|
|
1245
|
+
if (path[i] !== segments[i]) {
|
|
1246
|
+
matches = false;
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (matches) {
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Runs fn in zone.js' root zone when zone.js is loaded (Angular / Ionic apps), so the plugin's
|
|
1260
|
+
* timers and body reads do not trigger Angular change detection. Without zone.js it just runs fn.
|
|
1261
|
+
*/
|
|
1262
|
+
function runOutsideZone(fn) {
|
|
1263
|
+
const zone = typeof globalThis !== 'undefined'
|
|
1264
|
+
? globalThis.Zone
|
|
1265
|
+
: undefined;
|
|
1266
|
+
const root = zone === null || zone === void 0 ? void 0 : zone.root;
|
|
1267
|
+
if (root && typeof root.run === 'function') {
|
|
1268
|
+
return root.run(fn);
|
|
1269
|
+
}
|
|
1270
|
+
return fn();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const MAX_NETWORK_ENTRIES = 30;
|
|
1274
|
+
const now = () => typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
|
|
1275
|
+
/**
|
|
1276
|
+
* Records fetch and XMLHttpRequest calls made inside the WebView. The hooks never alter or delay the
|
|
1277
|
+
* request: they only read what the app passes in and what comes back, and every hook is guarded so
|
|
1278
|
+
* a logging failure cannot break the app's networking.
|
|
1279
|
+
*/
|
|
1280
|
+
class NetworkCapture {
|
|
1281
|
+
constructor(win, onChange) {
|
|
1282
|
+
this.win = win;
|
|
1283
|
+
this.onChange = onChange;
|
|
1284
|
+
this.records = [];
|
|
1285
|
+
this.nextId = 1;
|
|
1286
|
+
this.active = false;
|
|
1287
|
+
this.isInstalled = false;
|
|
1288
|
+
this.xhrStates = new WeakMap();
|
|
1289
|
+
this.xhrsWithListeners = new WeakSet();
|
|
1290
|
+
}
|
|
1291
|
+
/** Installs the hooks inactive: nothing is recorded until setActive(true). */
|
|
1292
|
+
install() {
|
|
1293
|
+
if (this.isInstalled) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
this.isInstalled = true;
|
|
1297
|
+
try {
|
|
1298
|
+
this.patchFetch();
|
|
1299
|
+
}
|
|
1300
|
+
catch (e) {
|
|
1301
|
+
// Ignore.
|
|
1302
|
+
}
|
|
1303
|
+
try {
|
|
1304
|
+
this.patchXhr();
|
|
1305
|
+
}
|
|
1306
|
+
catch (e) {
|
|
1307
|
+
// Ignore.
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/** Inactive: the hooks stay in place but only call through, and the buffer is dropped. */
|
|
1311
|
+
setActive(active) {
|
|
1312
|
+
this.active = active;
|
|
1313
|
+
if (!active) {
|
|
1314
|
+
this.records = [];
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
getSnapshots() {
|
|
1318
|
+
return this.records.map((record) => ({
|
|
1319
|
+
id: record.id,
|
|
1320
|
+
version: record.version,
|
|
1321
|
+
entry: record.entry,
|
|
1322
|
+
}));
|
|
1323
|
+
}
|
|
1324
|
+
// ---------------------------------------------------------------- fetch
|
|
1325
|
+
patchFetch() {
|
|
1326
|
+
const win = this.win;
|
|
1327
|
+
const originalFetch = win.fetch;
|
|
1328
|
+
if (typeof originalFetch !== 'function') {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const capture = this;
|
|
1332
|
+
win.fetch = function gleapFetch() {
|
|
1333
|
+
// eslint-disable-next-line prefer-rest-params
|
|
1334
|
+
const args = arguments;
|
|
1335
|
+
let record = null;
|
|
1336
|
+
if (capture.active) {
|
|
1337
|
+
try {
|
|
1338
|
+
record = capture.startFetch(args[0], args[1]);
|
|
1339
|
+
}
|
|
1340
|
+
catch (e) {
|
|
1341
|
+
record = null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
let result;
|
|
1345
|
+
try {
|
|
1346
|
+
result = originalFetch.apply(this, args);
|
|
1347
|
+
}
|
|
1348
|
+
catch (error) {
|
|
1349
|
+
if (record) {
|
|
1350
|
+
capture.fail(record, error);
|
|
1351
|
+
}
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
const promise = result;
|
|
1355
|
+
if (!record || !promise || typeof promise.then !== 'function') {
|
|
1356
|
+
return result;
|
|
1357
|
+
}
|
|
1358
|
+
const tracked = record;
|
|
1359
|
+
return promise.then((response) => {
|
|
1360
|
+
capture.completeFetch(tracked, response);
|
|
1361
|
+
return response;
|
|
1362
|
+
}, (error) => {
|
|
1363
|
+
capture.fail(tracked, error);
|
|
1364
|
+
throw error;
|
|
1365
|
+
});
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
startFetch(input, init) {
|
|
1369
|
+
const url = this.resolveUrl(isRequest(input) ? input.url : isUrl(input) ? input.href : input);
|
|
1370
|
+
if (!this.shouldCapture(url)) {
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
const request = isRequest(input) ? input : null;
|
|
1374
|
+
const options = init && typeof init === 'object' ? init : undefined;
|
|
1375
|
+
const method = String((options === null || options === void 0 ? void 0 : options.method) || (request === null || request === void 0 ? void 0 : request.method) || 'GET').toUpperCase();
|
|
1376
|
+
const headers = normalizeHeaders((options === null || options === void 0 ? void 0 : options.headers) !== undefined ? options.headers : request ? request.headers : undefined);
|
|
1377
|
+
let payload = '';
|
|
1378
|
+
if ((options === null || options === void 0 ? void 0 : options.body) !== undefined && (options === null || options === void 0 ? void 0 : options.body) !== null) {
|
|
1379
|
+
payload = this.requestPayload(options.body, headers);
|
|
1380
|
+
}
|
|
1381
|
+
else if (request && method !== 'GET' && method !== 'HEAD') {
|
|
1382
|
+
payload = this.requestObjectPayload(request, headers);
|
|
1383
|
+
}
|
|
1384
|
+
return this.createRecord(method, url, headers, payload);
|
|
1385
|
+
}
|
|
1386
|
+
/** The body of a Request object: read from a clone (made before fetch consumes it), head only. */
|
|
1387
|
+
requestObjectPayload(request, headers) {
|
|
1388
|
+
const kind = classifyContentType(headers['content-type']);
|
|
1389
|
+
const marker = markerForKind(kind);
|
|
1390
|
+
if (marker) {
|
|
1391
|
+
return marker;
|
|
1392
|
+
}
|
|
1393
|
+
if (typeof Request === 'undefined' || !('body' in Request.prototype)) {
|
|
1394
|
+
return BODY_NOT_CAPTURED$1;
|
|
1395
|
+
}
|
|
1396
|
+
if (request.body === null) {
|
|
1397
|
+
return '';
|
|
1398
|
+
}
|
|
1399
|
+
let copy;
|
|
1400
|
+
try {
|
|
1401
|
+
copy = request.clone();
|
|
1402
|
+
}
|
|
1403
|
+
catch (e) {
|
|
1404
|
+
return BODY_NOT_CAPTURED$1;
|
|
1405
|
+
}
|
|
1406
|
+
const stream = copy.body;
|
|
1407
|
+
if (!stream || typeof stream.getReader !== 'function') {
|
|
1408
|
+
return BODY_NOT_CAPTURED$1;
|
|
1409
|
+
}
|
|
1410
|
+
return runOutsideZone(() => readStreamHead(stream).then(({ bytes, complete }) => bytesToBodyText(bytes, kind, complete, null), () => BODY_NOT_CAPTURED$1));
|
|
1411
|
+
}
|
|
1412
|
+
completeFetch(record, response) {
|
|
1413
|
+
try {
|
|
1414
|
+
const headers = normalizeHeaders(response.headers);
|
|
1415
|
+
this.update(record, (entry) => {
|
|
1416
|
+
entry.duration = Math.round(now() - record.startedAt);
|
|
1417
|
+
entry.success = true;
|
|
1418
|
+
entry.response = {
|
|
1419
|
+
status: response.status,
|
|
1420
|
+
statusText: response.statusText || '',
|
|
1421
|
+
headers,
|
|
1422
|
+
responseText: BODY_PENDING,
|
|
1423
|
+
};
|
|
1424
|
+
});
|
|
1425
|
+
this.setResponseText(record, this.fetchResponseBody(response, headers));
|
|
1426
|
+
}
|
|
1427
|
+
catch (e) {
|
|
1428
|
+
// Ignore.
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Streaming and binary responses are never cloned. Text responses are cloned synchronously (before
|
|
1433
|
+
* the app sees the response) and only the head of the clone is read, then the clone is cancelled.
|
|
1434
|
+
*/
|
|
1435
|
+
fetchResponseBody(response, headers) {
|
|
1436
|
+
const kind = classifyContentType(headers['content-type']);
|
|
1437
|
+
const marker = markerForKind(kind);
|
|
1438
|
+
if (marker) {
|
|
1439
|
+
return marker;
|
|
1440
|
+
}
|
|
1441
|
+
if (response.type === 'opaque' || response.type === 'opaqueredirect') {
|
|
1442
|
+
return BODY_NOT_CAPTURED$1;
|
|
1443
|
+
}
|
|
1444
|
+
const body = response.body;
|
|
1445
|
+
if (body === null) {
|
|
1446
|
+
return '';
|
|
1447
|
+
}
|
|
1448
|
+
if (!body || typeof body.getReader !== 'function' || typeof response.clone !== 'function') {
|
|
1449
|
+
return BODY_NOT_CAPTURED$1;
|
|
1450
|
+
}
|
|
1451
|
+
let copy;
|
|
1452
|
+
try {
|
|
1453
|
+
copy = response.clone();
|
|
1454
|
+
}
|
|
1455
|
+
catch (e) {
|
|
1456
|
+
return BODY_NOT_CAPTURED$1;
|
|
1457
|
+
}
|
|
1458
|
+
const stream = copy.body;
|
|
1459
|
+
if (!stream) {
|
|
1460
|
+
return '';
|
|
1461
|
+
}
|
|
1462
|
+
const totalBytes = identityContentLength(headers);
|
|
1463
|
+
return runOutsideZone(() => readStreamHead(stream).then(({ bytes, complete }) => bytesToBodyText(bytes, kind, complete, complete ? null : totalBytes), () => BODY_NOT_CAPTURED$1));
|
|
1464
|
+
}
|
|
1465
|
+
// ------------------------------------------------------------------ XHR
|
|
1466
|
+
patchXhr() {
|
|
1467
|
+
const win = this.win;
|
|
1468
|
+
const Xhr = win.XMLHttpRequest;
|
|
1469
|
+
if (typeof Xhr !== 'function' || !Xhr || !Xhr.prototype) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const capacitorXhr = win.CapacitorWebXMLHttpRequest;
|
|
1473
|
+
if ((capacitorXhr === null || capacitorXhr === void 0 ? void 0 : capacitorXhr.fullObject) && Xhr !== capacitorXhr.fullObject) {
|
|
1474
|
+
// CapacitorHttp replaced the constructor and re-assigns the prototype methods on every
|
|
1475
|
+
// construction, so the methods are hooked per instance instead.
|
|
1476
|
+
this.wrapXhrConstructor(Xhr);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const proto = Xhr.prototype;
|
|
1480
|
+
const originals = {
|
|
1481
|
+
open: proto.open,
|
|
1482
|
+
send: proto.send,
|
|
1483
|
+
setRequestHeader: proto.setRequestHeader,
|
|
1484
|
+
};
|
|
1485
|
+
if (typeof originals.open !== 'function' ||
|
|
1486
|
+
typeof originals.send !== 'function' ||
|
|
1487
|
+
typeof originals.setRequestHeader !== 'function') {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
this.hookXhrMethods(proto, (_xhr, name) => originals[name]);
|
|
1491
|
+
}
|
|
1492
|
+
wrapXhrConstructor(Factory) {
|
|
1493
|
+
const capture = this;
|
|
1494
|
+
const Wrapped = function GleapXMLHttpRequest() {
|
|
1495
|
+
const xhr = new Factory();
|
|
1496
|
+
try {
|
|
1497
|
+
capture.hookXhrMethods(xhr, (instance, name) => Object.getPrototypeOf(instance)[name]);
|
|
1498
|
+
}
|
|
1499
|
+
catch (e) {
|
|
1500
|
+
// Ignore.
|
|
1501
|
+
}
|
|
1502
|
+
return xhr;
|
|
1503
|
+
};
|
|
1504
|
+
try {
|
|
1505
|
+
Object.assign(Wrapped, Factory);
|
|
1506
|
+
}
|
|
1507
|
+
catch (e) {
|
|
1508
|
+
// Ignore.
|
|
1509
|
+
}
|
|
1510
|
+
Wrapped.prototype = Factory.prototype;
|
|
1511
|
+
this.win.XMLHttpRequest = Wrapped;
|
|
1512
|
+
}
|
|
1513
|
+
hookXhrMethods(target, original) {
|
|
1514
|
+
const capture = this;
|
|
1515
|
+
target.open = function () {
|
|
1516
|
+
// eslint-disable-next-line prefer-rest-params
|
|
1517
|
+
const args = arguments;
|
|
1518
|
+
try {
|
|
1519
|
+
capture.onXhrOpen(this, args[0], args[1]);
|
|
1520
|
+
}
|
|
1521
|
+
catch (e) {
|
|
1522
|
+
// Ignore.
|
|
1523
|
+
}
|
|
1524
|
+
return original(this, 'open').apply(this, args);
|
|
1525
|
+
};
|
|
1526
|
+
target.setRequestHeader = function () {
|
|
1527
|
+
// eslint-disable-next-line prefer-rest-params
|
|
1528
|
+
const args = arguments;
|
|
1529
|
+
try {
|
|
1530
|
+
capture.onXhrHeader(this, args[0], args[1]);
|
|
1531
|
+
}
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
// Ignore.
|
|
1534
|
+
}
|
|
1535
|
+
return original(this, 'setRequestHeader').apply(this, args);
|
|
1536
|
+
};
|
|
1537
|
+
target.send = function () {
|
|
1538
|
+
// eslint-disable-next-line prefer-rest-params
|
|
1539
|
+
const args = arguments;
|
|
1540
|
+
let record = null;
|
|
1541
|
+
try {
|
|
1542
|
+
record = capture.onXhrSend(this, args[0]);
|
|
1543
|
+
}
|
|
1544
|
+
catch (e) {
|
|
1545
|
+
record = null;
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
return original(this, 'send').apply(this, args);
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
if (record) {
|
|
1552
|
+
capture.fail(record, error);
|
|
1553
|
+
const state = capture.xhrStates.get(this);
|
|
1554
|
+
if (state && state.record === record) {
|
|
1555
|
+
state.record = null;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
throw error;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
onXhrOpen(xhr, method, url) {
|
|
1563
|
+
this.xhrStates.set(xhr, {
|
|
1564
|
+
method: String(method || 'GET').toUpperCase(),
|
|
1565
|
+
url: this.resolveUrl(isUrl(url) ? url.href : url),
|
|
1566
|
+
headers: {},
|
|
1567
|
+
record: null,
|
|
1568
|
+
headersAt: null,
|
|
1569
|
+
failure: null,
|
|
1570
|
+
loaded: false,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
onXhrHeader(xhr, name, value) {
|
|
1574
|
+
const state = this.xhrStates.get(xhr);
|
|
1575
|
+
if (state && typeof name === 'string') {
|
|
1576
|
+
addHeader(state.headers, name, String(value));
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
onXhrSend(xhr, body) {
|
|
1580
|
+
if (!this.active) {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
const state = this.xhrStates.get(xhr);
|
|
1584
|
+
if (!state || !this.shouldCapture(state.url)) {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
this.listenToXhr(xhr);
|
|
1588
|
+
state.failure = null;
|
|
1589
|
+
state.loaded = false;
|
|
1590
|
+
state.headersAt = null;
|
|
1591
|
+
const headers = Object.assign({}, state.headers);
|
|
1592
|
+
const payload = state.method === 'GET' || state.method === 'HEAD' ? '' : this.requestPayload(body, headers);
|
|
1593
|
+
state.record = this.createRecord(state.method, state.url, headers, payload);
|
|
1594
|
+
return state.record;
|
|
1595
|
+
}
|
|
1596
|
+
listenToXhr(xhr) {
|
|
1597
|
+
if (this.xhrsWithListeners.has(xhr)) {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
this.xhrsWithListeners.add(xhr);
|
|
1601
|
+
const state = () => this.xhrStates.get(xhr);
|
|
1602
|
+
const guard = (fn) => () => {
|
|
1603
|
+
try {
|
|
1604
|
+
const current = state();
|
|
1605
|
+
if (current === null || current === void 0 ? void 0 : current.record) {
|
|
1606
|
+
fn(current);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
catch (e) {
|
|
1610
|
+
// Ignore.
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
xhr.addEventListener('readystatechange', guard((current) => {
|
|
1614
|
+
if (current.headersAt === null && xhr.readyState >= 2) {
|
|
1615
|
+
current.headersAt = now();
|
|
1616
|
+
}
|
|
1617
|
+
}));
|
|
1618
|
+
xhr.addEventListener('load', guard((current) => (current.loaded = true)));
|
|
1619
|
+
xhr.addEventListener('abort', guard((current) => (current.failure = 'abort')));
|
|
1620
|
+
xhr.addEventListener('timeout', guard((current) => (current.failure = 'timeout')));
|
|
1621
|
+
xhr.addEventListener('error', guard((current) => (current.failure = 'error')));
|
|
1622
|
+
xhr.addEventListener('loadend', guard((current) => this.onXhrLoadEnd(xhr, current)));
|
|
1623
|
+
}
|
|
1624
|
+
onXhrLoadEnd(xhr, state) {
|
|
1625
|
+
const record = state.record;
|
|
1626
|
+
if (!record) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
state.record = null;
|
|
1630
|
+
const finishedAt = now();
|
|
1631
|
+
if (state.failure || !state.loaded) {
|
|
1632
|
+
let errorText = 'Network request failed';
|
|
1633
|
+
if (state.failure === 'abort') {
|
|
1634
|
+
errorText = 'Request aborted';
|
|
1635
|
+
}
|
|
1636
|
+
else if (state.failure === 'timeout') {
|
|
1637
|
+
errorText = xhr.timeout > 0 ? `Request timed out after ${xhr.timeout} ms` : 'Request timed out';
|
|
1638
|
+
}
|
|
1639
|
+
this.update(record, (entry) => {
|
|
1640
|
+
entry.duration = Math.round(finishedAt - record.startedAt);
|
|
1641
|
+
entry.success = false;
|
|
1642
|
+
entry.response = { errorText };
|
|
1643
|
+
});
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
let rawHeaders = '';
|
|
1647
|
+
try {
|
|
1648
|
+
rawHeaders = xhr.getAllResponseHeaders();
|
|
1649
|
+
}
|
|
1650
|
+
catch (e) {
|
|
1651
|
+
rawHeaders = '';
|
|
1652
|
+
}
|
|
1653
|
+
const headers = parseRawHeaders(rawHeaders);
|
|
1654
|
+
this.update(record, (entry) => {
|
|
1655
|
+
entry.duration = Math.round((state.headersAt !== null ? state.headersAt : finishedAt) - record.startedAt);
|
|
1656
|
+
entry.success = true;
|
|
1657
|
+
entry.response = {
|
|
1658
|
+
status: xhr.status,
|
|
1659
|
+
statusText: xhr.statusText || '',
|
|
1660
|
+
headers,
|
|
1661
|
+
responseText: BODY_PENDING,
|
|
1662
|
+
};
|
|
1663
|
+
});
|
|
1664
|
+
let body;
|
|
1665
|
+
try {
|
|
1666
|
+
body = this.xhrResponseBody(xhr, headers);
|
|
1667
|
+
}
|
|
1668
|
+
catch (e) {
|
|
1669
|
+
body = BODY_NOT_CAPTURED$1;
|
|
1670
|
+
}
|
|
1671
|
+
this.setResponseText(record, body);
|
|
1672
|
+
}
|
|
1673
|
+
/** The response is already in memory; only a bounded part of it is turned into text. */
|
|
1674
|
+
xhrResponseBody(xhr, headers) {
|
|
1675
|
+
const kind = classifyContentType(headers['content-type']);
|
|
1676
|
+
if (kind === 'streaming') {
|
|
1677
|
+
return STREAMING_BODY_OMITTED;
|
|
1678
|
+
}
|
|
1679
|
+
const responseType = xhr.responseType || '';
|
|
1680
|
+
if (kind === 'binary') {
|
|
1681
|
+
return BINARY_BODY_OMITTED;
|
|
1682
|
+
}
|
|
1683
|
+
if (responseType === '' || responseType === 'text') {
|
|
1684
|
+
const text = xhr.responseText;
|
|
1685
|
+
if (typeof text !== 'string') {
|
|
1686
|
+
return text === null || text === undefined ? '' : stringifyJsonHead(text);
|
|
1687
|
+
}
|
|
1688
|
+
if (kind === 'unknown' && text.indexOf('�') > -1) {
|
|
1689
|
+
// The browser had to replace invalid UTF-8: not a text body.
|
|
1690
|
+
return BINARY_BODY_OMITTED;
|
|
1691
|
+
}
|
|
1692
|
+
return capBodyText(text);
|
|
1693
|
+
}
|
|
1694
|
+
if (responseType === 'json') {
|
|
1695
|
+
const value = xhr.response;
|
|
1696
|
+
if (value === null || value === undefined) {
|
|
1697
|
+
return BODY_NOT_CAPTURED$1;
|
|
1698
|
+
}
|
|
1699
|
+
return typeof value === 'string' ? capBodyText(value) : stringifyJsonHead(value);
|
|
1700
|
+
}
|
|
1701
|
+
if (responseType === 'arraybuffer') {
|
|
1702
|
+
const bytes = toBytes(xhr.response);
|
|
1703
|
+
if (!bytes) {
|
|
1704
|
+
return BODY_NOT_CAPTURED$1;
|
|
1705
|
+
}
|
|
1706
|
+
return bytesToBodyText(bytes, kind, bytes.byteLength <= MAX_BODY_SIZE, bytes.byteLength);
|
|
1707
|
+
}
|
|
1708
|
+
if (responseType === 'blob') {
|
|
1709
|
+
const blob = xhr.response;
|
|
1710
|
+
if (!isBlob(blob)) {
|
|
1711
|
+
return BODY_NOT_CAPTURED$1;
|
|
1712
|
+
}
|
|
1713
|
+
return runOutsideZone(() => readBlobHead(blob, kind).catch(() => BODY_NOT_CAPTURED$1));
|
|
1714
|
+
}
|
|
1715
|
+
return BODY_NOT_CAPTURED$1;
|
|
1716
|
+
}
|
|
1717
|
+
// --------------------------------------------------------------- shared
|
|
1718
|
+
requestPayload(body, headers) {
|
|
1719
|
+
if (isUrlSearchParams(body) && !hasOwn(headers, 'content-type')) {
|
|
1720
|
+
// Sent with this content type by the browser; recorded so form fields can be redacted.
|
|
1721
|
+
headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
|
1722
|
+
}
|
|
1723
|
+
const kind = classifyContentType(headers['content-type']);
|
|
1724
|
+
return runOutsideZone(() => captureRequestBody(body, kind));
|
|
1725
|
+
}
|
|
1726
|
+
createRecord(method, url, headers, payload) {
|
|
1727
|
+
const record = {
|
|
1728
|
+
id: this.nextId++,
|
|
1729
|
+
version: 0,
|
|
1730
|
+
startedAt: now(),
|
|
1731
|
+
entry: {
|
|
1732
|
+
date: new Date().toISOString(),
|
|
1733
|
+
type: method,
|
|
1734
|
+
url,
|
|
1735
|
+
request: {
|
|
1736
|
+
headers,
|
|
1737
|
+
payload: typeof payload === 'string' ? payload : BODY_PENDING,
|
|
1738
|
+
},
|
|
1739
|
+
},
|
|
1740
|
+
};
|
|
1741
|
+
if (typeof payload !== 'string') {
|
|
1742
|
+
const setPayload = (text) => this.update(record, (entry) => {
|
|
1743
|
+
if (entry.request) {
|
|
1744
|
+
entry.request.payload = text;
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
runOutsideZone(() => payload.then(setPayload, () => setPayload(BODY_NOT_CAPTURED$1)));
|
|
1748
|
+
}
|
|
1749
|
+
this.records.push(record);
|
|
1750
|
+
if (this.records.length > MAX_NETWORK_ENTRIES) {
|
|
1751
|
+
this.records.splice(0, this.records.length - MAX_NETWORK_ENTRIES);
|
|
1752
|
+
}
|
|
1753
|
+
this.onChange();
|
|
1754
|
+
return record;
|
|
1755
|
+
}
|
|
1756
|
+
setResponseText(record, body) {
|
|
1757
|
+
const apply = (text) => this.update(record, (entry) => {
|
|
1758
|
+
if (entry.response && entry.response.errorText === undefined) {
|
|
1759
|
+
entry.response.responseText = text;
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
if (typeof body === 'string') {
|
|
1763
|
+
apply(body);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
runOutsideZone(() => body.then(apply, () => apply(BODY_NOT_CAPTURED$1)));
|
|
1767
|
+
}
|
|
1768
|
+
fail(record, error) {
|
|
1769
|
+
try {
|
|
1770
|
+
this.update(record, (entry) => {
|
|
1771
|
+
entry.duration = Math.round(now() - record.startedAt);
|
|
1772
|
+
entry.success = false;
|
|
1773
|
+
entry.response = { errorText: describeError(error) };
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
catch (e) {
|
|
1777
|
+
// Ignore.
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
update(record, mutate) {
|
|
1781
|
+
try {
|
|
1782
|
+
mutate(record.entry);
|
|
1783
|
+
}
|
|
1784
|
+
catch (e) {
|
|
1785
|
+
// Ignore.
|
|
1786
|
+
}
|
|
1787
|
+
record.version++;
|
|
1788
|
+
if (this.active && this.records.indexOf(record) > -1) {
|
|
1789
|
+
this.onChange();
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
resolveUrl(url) {
|
|
1793
|
+
const raw = typeof url === 'string' ? url : String(url);
|
|
1794
|
+
try {
|
|
1795
|
+
const doc = this.win.document;
|
|
1796
|
+
const base = (doc === null || doc === void 0 ? void 0 : doc.baseURI) || this.win.location.href;
|
|
1797
|
+
return new URL(raw, base).href;
|
|
1798
|
+
}
|
|
1799
|
+
catch (e) {
|
|
1800
|
+
return raw;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Only real network traffic: http(s) URLs that are not Gleap's own and not the app's bundled files
|
|
1805
|
+
* served by Capacitor's local server (http(s)://localhost on Android; capacitor:// on iOS).
|
|
1806
|
+
*/
|
|
1807
|
+
shouldCapture(url) {
|
|
1808
|
+
const lower = url.toLowerCase();
|
|
1809
|
+
if (lower.indexOf('http://') !== 0 && lower.indexOf('https://') !== 0) {
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
for (const host of DEFAULT_NETWORK_LOG_BLACKLIST) {
|
|
1813
|
+
if (lower.indexOf(host) > -1) {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
try {
|
|
1818
|
+
const location = this.win.location;
|
|
1819
|
+
if (location.hostname === 'localhost' && !location.port) {
|
|
1820
|
+
return new URL(url).origin !== location.origin;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
catch (e) {
|
|
1824
|
+
// Ignore.
|
|
1825
|
+
}
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function isRequest(value) {
|
|
1830
|
+
return typeof Request !== 'undefined' && value instanceof Request;
|
|
1831
|
+
}
|
|
1832
|
+
function isUrl(value) {
|
|
1833
|
+
return typeof URL !== 'undefined' && value instanceof URL;
|
|
1834
|
+
}
|
|
1835
|
+
function hasOwn(object, key) {
|
|
1836
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
1837
|
+
}
|
|
1838
|
+
function addHeader(headers, name, value) {
|
|
1839
|
+
const key = name.trim().toLowerCase();
|
|
1840
|
+
if (!key || key === '__proto__') {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
headers[key] = hasOwn(headers, key) ? `${headers[key]}, ${value}` : value;
|
|
1844
|
+
}
|
|
1845
|
+
/** Headers | [name, value][] | record -> { lowercased name: value }, repeated names joined with ", ". */
|
|
1846
|
+
function normalizeHeaders(headers) {
|
|
1847
|
+
const result = {};
|
|
1848
|
+
if (!headers) {
|
|
1849
|
+
return result;
|
|
1850
|
+
}
|
|
1851
|
+
try {
|
|
1852
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
|
1853
|
+
headers.forEach((value, name) => addHeader(result, name, value));
|
|
1854
|
+
return result;
|
|
1855
|
+
}
|
|
1856
|
+
if (Array.isArray(headers)) {
|
|
1857
|
+
for (const pair of headers) {
|
|
1858
|
+
if (pair && pair.length >= 2) {
|
|
1859
|
+
addHeader(result, String(pair[0]), String(pair[1]));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return result;
|
|
1863
|
+
}
|
|
1864
|
+
if (typeof headers === 'object') {
|
|
1865
|
+
const record = headers;
|
|
1866
|
+
for (const name of Object.keys(record)) {
|
|
1867
|
+
const value = record[name];
|
|
1868
|
+
if (value !== undefined && value !== null) {
|
|
1869
|
+
addHeader(result, name, String(value));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
catch (e) {
|
|
1875
|
+
// Ignore.
|
|
1876
|
+
}
|
|
1877
|
+
return result;
|
|
1878
|
+
}
|
|
1879
|
+
function parseRawHeaders(raw) {
|
|
1880
|
+
const result = {};
|
|
1881
|
+
if (typeof raw !== 'string') {
|
|
1882
|
+
return result;
|
|
1883
|
+
}
|
|
1884
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1885
|
+
const index = line.indexOf(':');
|
|
1886
|
+
if (index > 0) {
|
|
1887
|
+
addHeader(result, line.slice(0, index), line.slice(index + 1).trim());
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return result;
|
|
1891
|
+
}
|
|
1892
|
+
/** content-length when it describes the decoded body (no content-encoding), else null. */
|
|
1893
|
+
function identityContentLength(headers) {
|
|
1894
|
+
const encoding = (headers['content-encoding'] || '').trim().toLowerCase();
|
|
1895
|
+
if (encoding && encoding !== 'identity') {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
const length = parseInt(headers['content-length'] || '', 10);
|
|
1899
|
+
return isFinite(length) && length >= 0 ? length : null;
|
|
1900
|
+
}
|
|
1901
|
+
function describeError(error) {
|
|
1902
|
+
try {
|
|
1903
|
+
if (error && typeof error === 'object') {
|
|
1904
|
+
const { name, message } = error;
|
|
1905
|
+
const text = typeof message === 'string' ? message : '';
|
|
1906
|
+
if (name === 'AbortError') {
|
|
1907
|
+
return text ? `Request aborted (${text})` : 'Request aborted';
|
|
1908
|
+
}
|
|
1909
|
+
if (name === 'TimeoutError') {
|
|
1910
|
+
return text ? `Request timed out (${text})` : 'Request timed out';
|
|
1911
|
+
}
|
|
1912
|
+
if (text) {
|
|
1913
|
+
return typeof name === 'string' && name ? `${name}: ${text}` : text;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const text = String(error);
|
|
1917
|
+
return text && text !== 'undefined' ? text : 'Request failed';
|
|
1918
|
+
}
|
|
1919
|
+
catch (e) {
|
|
1920
|
+
return 'Request failed';
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/** Pushes are batched: at most one per channel every PUSH_DELAY_MS. */
|
|
1925
|
+
const PUSH_DELAY_MS = 500;
|
|
1926
|
+
/** A channel stops pushing after this many failed pushes in a row (e.g. an outdated native plugin). */
|
|
1927
|
+
const MAX_FAILED_PUSHES = 3;
|
|
1928
|
+
const GLOBAL_KEY = '__gleapWebViewLogCapture';
|
|
1929
|
+
/** Plugin calls that can lead to a report: the latest logs are pushed right before them. */
|
|
1930
|
+
const FLUSH_BEFORE_METHODS = [
|
|
1931
|
+
'sendSilentCrashReport',
|
|
1932
|
+
'open',
|
|
1933
|
+
'startFeedbackFlow',
|
|
1934
|
+
'startClassicForm',
|
|
1935
|
+
'startConversation',
|
|
1936
|
+
'openConversation',
|
|
1937
|
+
'openConversations',
|
|
1938
|
+
'startBot',
|
|
1939
|
+
];
|
|
1940
|
+
const createChannel = (name) => ({
|
|
1941
|
+
name,
|
|
1942
|
+
isDirty: false,
|
|
1943
|
+
timer: null,
|
|
1944
|
+
failures: 0,
|
|
1945
|
+
isStopped: false,
|
|
1946
|
+
hasPushedLogs: false,
|
|
1947
|
+
});
|
|
1948
|
+
const toStringArray = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
1949
|
+
/**
|
|
1950
|
+
* Captures console output and fetch/XHR traffic inside the WebView of a native (iOS / Android) app
|
|
1951
|
+
* and hands them to the native SDK, which attaches them to tickets. The native SDKs cannot see
|
|
1952
|
+
* either: WebView requests do not go through URLSession / OkHttp, and console output only reaches
|
|
1953
|
+
* the native log in debug builds.
|
|
1954
|
+
*/
|
|
1955
|
+
class WebViewLogCapture {
|
|
1956
|
+
constructor(win, target, platform) {
|
|
1957
|
+
this.win = win;
|
|
1958
|
+
this.target = target;
|
|
1959
|
+
this.platform = platform;
|
|
1960
|
+
this.consoleCapture = null;
|
|
1961
|
+
this.consoleChannel = createChannel('console');
|
|
1962
|
+
this.networkChannel = createChannel('network');
|
|
1963
|
+
this.isNetworkEnabled = false;
|
|
1964
|
+
this.remoteProps = [];
|
|
1965
|
+
this.localProps = [];
|
|
1966
|
+
this.remoteBlacklist = [];
|
|
1967
|
+
this.localBlacklist = [];
|
|
1968
|
+
this.rules = createRedactionRules([], []);
|
|
1969
|
+
this.rulesVersion = 0;
|
|
1970
|
+
this.redactedEntries = new Map();
|
|
1971
|
+
this.networkCapture = new NetworkCapture(win, () => this.markDirty(this.networkChannel));
|
|
1972
|
+
}
|
|
1973
|
+
start() {
|
|
1974
|
+
if (!this.nativeLogsConsole()) {
|
|
1975
|
+
this.consoleCapture = new ConsoleCapture(this.win, () => this.markDirty(this.consoleChannel));
|
|
1976
|
+
this.consoleCapture.install();
|
|
1977
|
+
}
|
|
1978
|
+
// The hooks go in now, but requests are only recorded once the project config says network
|
|
1979
|
+
// logs are enabled (like the JavaScript SDK), so nothing is held in memory before that.
|
|
1980
|
+
this.networkCapture.install();
|
|
1981
|
+
this.listenForConfig();
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Android debug builds (Capacitor's loggingBehavior) write the WebView console to logcat, which the
|
|
1985
|
+
* Android SDK already reads: capturing it here too would log every line twice. On iOS, Capacitor
|
|
1986
|
+
* prints it to stdout as "⚡️ [level] - message"; the iOS SDK drops those lines for Capacitor apps
|
|
1987
|
+
* (application type CAPACITOR, set in initialize), so iOS always captures here.
|
|
1988
|
+
*/
|
|
1989
|
+
nativeLogsConsole() {
|
|
1990
|
+
const capacitor = this.win.Capacitor;
|
|
1991
|
+
return this.platform === 'android' && !!capacitor && capacitor.isLoggingEnabled === true;
|
|
1992
|
+
}
|
|
1993
|
+
listenForConfig() {
|
|
1994
|
+
try {
|
|
1995
|
+
const handle = this.target.addListener('logConfigLoaded', (config) => this.applyRemoteConfig(config));
|
|
1996
|
+
if (handle && typeof handle.catch === 'function') {
|
|
1997
|
+
handle.catch(() => undefined);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
catch (e) {
|
|
2001
|
+
// The native side predates the event: network logs stay off.
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
applyRemoteConfig(config) {
|
|
2005
|
+
try {
|
|
2006
|
+
if (!config || typeof config !== 'object') {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.remoteProps = toStringArray(config.networkLogPropsToIgnore);
|
|
2010
|
+
this.remoteBlacklist = toStringArray(config.networkLogBlacklist);
|
|
2011
|
+
this.updateRules();
|
|
2012
|
+
this.isNetworkEnabled = config.enableNetworkLogs === true;
|
|
2013
|
+
this.networkCapture.setActive(this.isNetworkEnabled);
|
|
2014
|
+
this.markDirty(this.networkChannel);
|
|
2015
|
+
}
|
|
2016
|
+
catch (e) {
|
|
2017
|
+
// Ignore.
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
setLocalPropsToIgnore(props) {
|
|
2021
|
+
this.localProps = toStringArray(props);
|
|
2022
|
+
this.updateRules();
|
|
2023
|
+
}
|
|
2024
|
+
setLocalBlacklist(blacklist) {
|
|
2025
|
+
this.localBlacklist = toStringArray(blacklist);
|
|
2026
|
+
this.updateRules();
|
|
2027
|
+
}
|
|
2028
|
+
/** disableConsoleLogOverwrite(): restore the console and drop what was recorded (also natively). */
|
|
2029
|
+
disableConsole() {
|
|
2030
|
+
try {
|
|
2031
|
+
if (this.consoleCapture) {
|
|
2032
|
+
this.consoleCapture.uninstall();
|
|
2033
|
+
this.consoleCapture = null;
|
|
2034
|
+
}
|
|
2035
|
+
this.markDirty(this.consoleChannel);
|
|
2036
|
+
this.flushChannel(this.consoleChannel);
|
|
2037
|
+
}
|
|
2038
|
+
catch (e) {
|
|
2039
|
+
// Ignore.
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
onInitialize() {
|
|
2043
|
+
this.markDirty(this.consoleChannel);
|
|
2044
|
+
this.markDirty(this.networkChannel);
|
|
2045
|
+
}
|
|
2046
|
+
/** Pushes pending changes now (before a plugin call that can produce a report). */
|
|
2047
|
+
flushNow() {
|
|
2048
|
+
try {
|
|
2049
|
+
this.flushChannel(this.consoleChannel);
|
|
2050
|
+
this.flushChannel(this.networkChannel);
|
|
2051
|
+
}
|
|
2052
|
+
catch (e) {
|
|
2053
|
+
// Ignore.
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
updateRules() {
|
|
2057
|
+
this.rules = createRedactionRules([...this.remoteProps, ...this.localProps], [...this.remoteBlacklist, ...this.localBlacklist]);
|
|
2058
|
+
this.rulesVersion++;
|
|
2059
|
+
this.markDirty(this.networkChannel);
|
|
2060
|
+
}
|
|
2061
|
+
canPush(channel) {
|
|
2062
|
+
if (channel.isStopped) {
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
if (channel.name === 'network') {
|
|
2066
|
+
return this.isNetworkEnabled || channel.hasPushedLogs;
|
|
2067
|
+
}
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
markDirty(channel) {
|
|
2071
|
+
channel.isDirty = true;
|
|
2072
|
+
if (channel.timer !== null || !this.canPush(channel)) {
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
try {
|
|
2076
|
+
channel.timer = runOutsideZone(() => setTimeout(() => {
|
|
2077
|
+
channel.timer = null;
|
|
2078
|
+
this.flushChannel(channel);
|
|
2079
|
+
}, PUSH_DELAY_MS));
|
|
2080
|
+
}
|
|
2081
|
+
catch (e) {
|
|
2082
|
+
channel.timer = null;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
flushChannel(channel) {
|
|
2086
|
+
if (channel.timer !== null) {
|
|
2087
|
+
clearTimeout(channel.timer);
|
|
2088
|
+
channel.timer = null;
|
|
2089
|
+
}
|
|
2090
|
+
if (!channel.isDirty || !this.canPush(channel)) {
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
channel.isDirty = false;
|
|
2094
|
+
let push;
|
|
2095
|
+
if (channel.name === 'console') {
|
|
2096
|
+
const logs = this.consoleCapture ? this.consoleCapture.getEntries() : [];
|
|
2097
|
+
if (logs.length === 0 && !channel.hasPushedLogs) {
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
channel.hasPushedLogs = logs.length > 0;
|
|
2101
|
+
push = runOutsideZone(() => this.target.attachConsoleLogs({ logs }));
|
|
2102
|
+
}
|
|
2103
|
+
else {
|
|
2104
|
+
const logs = this.isNetworkEnabled ? this.buildNetworkLogs() : [];
|
|
2105
|
+
if (logs.length === 0 && !channel.hasPushedLogs) {
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
channel.hasPushedLogs = logs.length > 0;
|
|
2109
|
+
push = runOutsideZone(() => this.target.attachNetworkLogs({ logs }));
|
|
2110
|
+
}
|
|
2111
|
+
try {
|
|
2112
|
+
Promise.resolve(push).then(() => {
|
|
2113
|
+
channel.failures = 0;
|
|
2114
|
+
}, () => this.onPushFailed(channel));
|
|
2115
|
+
}
|
|
2116
|
+
catch (e) {
|
|
2117
|
+
this.onPushFailed(channel);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
onPushFailed(channel) {
|
|
2121
|
+
channel.failures++;
|
|
2122
|
+
if (channel.failures >= MAX_FAILED_PUSHES) {
|
|
2123
|
+
channel.isStopped = true;
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
this.markDirty(channel);
|
|
2127
|
+
}
|
|
2128
|
+
/** Redacted copies of the buffered entries; unchanged entries reuse their previous copy. */
|
|
2129
|
+
buildNetworkLogs() {
|
|
2130
|
+
const logs = [];
|
|
2131
|
+
const cache = new Map();
|
|
2132
|
+
for (const snapshot of this.networkCapture.getSnapshots()) {
|
|
2133
|
+
let cached = this.redactedEntries.get(snapshot.id);
|
|
2134
|
+
if (!cached || cached.version !== snapshot.version || cached.rulesVersion !== this.rulesVersion) {
|
|
2135
|
+
cached = {
|
|
2136
|
+
version: snapshot.version,
|
|
2137
|
+
rulesVersion: this.rulesVersion,
|
|
2138
|
+
entry: isBlacklistedUrl(snapshot.entry.url, this.rules)
|
|
2139
|
+
? null
|
|
2140
|
+
: redactNetworkLogEntryOrStrip(snapshot.entry, this.rules),
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
cache.set(snapshot.id, cached);
|
|
2144
|
+
if (cached.entry) {
|
|
2145
|
+
logs.push(cached.entry);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
this.redactedEntries = cache;
|
|
2149
|
+
return logs;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Starts the WebView log capture on iOS and Android (not on web, where the JavaScript SDK records
|
|
2154
|
+
* the console and network itself). Safe to call more than once.
|
|
2155
|
+
*/
|
|
2156
|
+
function installWebViewLogCapture(target) {
|
|
2157
|
+
try {
|
|
2158
|
+
if (typeof window === 'undefined' || !core.Capacitor.isNativePlatform()) {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
const win = window;
|
|
2162
|
+
const existing = win[GLOBAL_KEY];
|
|
2163
|
+
if (existing) {
|
|
2164
|
+
return existing;
|
|
2165
|
+
}
|
|
2166
|
+
const capture = new WebViewLogCapture(window, target, core.Capacitor.getPlatform());
|
|
2167
|
+
win[GLOBAL_KEY] = capture;
|
|
2168
|
+
capture.start();
|
|
2169
|
+
return capture;
|
|
2170
|
+
}
|
|
2171
|
+
catch (e) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Wraps the registered plugin so the calls that affect the WebView capture reach it before they go
|
|
2177
|
+
* to native. Every other call passes through unchanged.
|
|
2178
|
+
*/
|
|
2179
|
+
function wrapPluginWithLogCapture(plugin, capture) {
|
|
2180
|
+
return new Proxy(plugin, {
|
|
2181
|
+
get(target, property, receiver) {
|
|
2182
|
+
const value = Reflect.get(target, property, receiver);
|
|
2183
|
+
if (typeof property !== 'string' || typeof value !== 'function') {
|
|
2184
|
+
return value;
|
|
2185
|
+
}
|
|
2186
|
+
const method = value;
|
|
2187
|
+
const before = (hook) => (...args) => {
|
|
2188
|
+
try {
|
|
2189
|
+
const options = args[0];
|
|
2190
|
+
hook(options && typeof options === 'object' ? options : {});
|
|
2191
|
+
}
|
|
2192
|
+
catch (e) {
|
|
2193
|
+
// Never break the plugin call.
|
|
2194
|
+
}
|
|
2195
|
+
return method(...args);
|
|
2196
|
+
};
|
|
2197
|
+
switch (property) {
|
|
2198
|
+
case 'initialize':
|
|
2199
|
+
return before(() => capture.onInitialize());
|
|
2200
|
+
case 'setNetworkLogPropsToIgnore':
|
|
2201
|
+
return before((options) => capture.setLocalPropsToIgnore(options.propsToIgnore));
|
|
2202
|
+
case 'setNetworkLogsBlacklist':
|
|
2203
|
+
return before((options) => capture.setLocalBlacklist(options.blacklist));
|
|
2204
|
+
case 'disableConsoleLogOverwrite':
|
|
2205
|
+
return before(() => capture.disableConsole());
|
|
2206
|
+
default:
|
|
2207
|
+
if (FLUSH_BEFORE_METHODS.indexOf(property) > -1) {
|
|
2208
|
+
return before(() => capture.flushNow());
|
|
2209
|
+
}
|
|
2210
|
+
return value;
|
|
2211
|
+
}
|
|
2212
|
+
},
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
const GleapNative = core.registerPlugin('Gleap', {
|
|
5
2217
|
web: () => Promise.resolve().then(function () { return web; }).then(m => new m.GleapWeb()),
|
|
6
2218
|
});
|
|
2219
|
+
// On iOS and Android, record the WebView's console from the moment the plugin is imported (and its
|
|
2220
|
+
// fetch/XHR traffic once the project config enables network logs), and hand both to the native SDK,
|
|
2221
|
+
// which cannot see either on its own.
|
|
2222
|
+
const webViewLogCapture = installWebViewLogCapture(GleapNative);
|
|
2223
|
+
const Gleap = webViewLogCapture
|
|
2224
|
+
? wrapPluginWithLogCapture(GleapNative, webViewLogCapture)
|
|
2225
|
+
: GleapNative;
|
|
7
2226
|
const registeredAgentTools = {};
|
|
8
2227
|
let agentToolListenerAttached = false;
|
|
9
2228
|
/**
|
|
@@ -216,6 +2435,10 @@ var capacitorGleap = (function (exports, core, Gleap$1) {
|
|
|
216
2435
|
Gleap$1.openConversations(options.showBackButton);
|
|
217
2436
|
return { conversationsOpened: true };
|
|
218
2437
|
}
|
|
2438
|
+
async openConversations(options) {
|
|
2439
|
+
Gleap$1.openConversations(options.showBackButton);
|
|
2440
|
+
return { conversationsOpened: true };
|
|
2441
|
+
}
|
|
219
2442
|
async showSurvey(options) {
|
|
220
2443
|
Gleap$1.showSurvey(options.surveyId, options.format);
|
|
221
2444
|
return { opened: true };
|
|
@@ -234,6 +2457,16 @@ var capacitorGleap = (function (exports, core, Gleap$1) {
|
|
|
234
2457
|
Gleap$1.setDisableEnvData((_a = options.disableEnvData) !== null && _a !== void 0 ? _a : false);
|
|
235
2458
|
return { envDataDisabled: true };
|
|
236
2459
|
}
|
|
2460
|
+
async setColorScheme(options) {
|
|
2461
|
+
if (!(options === null || options === void 0 ? void 0 : options.colorScheme)) {
|
|
2462
|
+
throw new Error('No colorScheme provided');
|
|
2463
|
+
}
|
|
2464
|
+
Gleap$1.setColorScheme(options.colorScheme, {
|
|
2465
|
+
lightBackgroundColor: options.lightBackgroundColor,
|
|
2466
|
+
darkBackgroundColor: options.darkBackgroundColor,
|
|
2467
|
+
});
|
|
2468
|
+
return { colorScheme: options.colorScheme };
|
|
2469
|
+
}
|
|
237
2470
|
async identify(options) {
|
|
238
2471
|
var userData = {
|
|
239
2472
|
name: options.name,
|
|
@@ -267,6 +2500,14 @@ var capacitorGleap = (function (exports, core, Gleap$1) {
|
|
|
267
2500
|
Gleap$1.setNetworkLogPropsToIgnore(options.propsToIgnore);
|
|
268
2501
|
return { propsToIgnoreSet: true };
|
|
269
2502
|
}
|
|
2503
|
+
async attachNetworkLogs() {
|
|
2504
|
+
// The JavaScript SDK records the page's requests itself on web.
|
|
2505
|
+
return { networkLogsAttached: false };
|
|
2506
|
+
}
|
|
2507
|
+
async attachConsoleLogs() {
|
|
2508
|
+
// The JavaScript SDK records the page's console itself on web.
|
|
2509
|
+
return { consoleLogsAttached: false };
|
|
2510
|
+
}
|
|
270
2511
|
async setEnvDataPropsToIgnore(options) {
|
|
271
2512
|
Gleap$1.setEnvDataPropsToIgnore(options.propsToIgnore);
|
|
272
2513
|
return { envDataPropsToIgnoreSet: true };
|
|
@@ -344,6 +2585,18 @@ var capacitorGleap = (function (exports, core, Gleap$1) {
|
|
|
344
2585
|
Gleap$1.open();
|
|
345
2586
|
return { openedWidget: true };
|
|
346
2587
|
}
|
|
2588
|
+
async openChecklists(options) {
|
|
2589
|
+
Gleap$1.openChecklists(options.showBackButton);
|
|
2590
|
+
return { opened: true };
|
|
2591
|
+
}
|
|
2592
|
+
async openChecklist(options) {
|
|
2593
|
+
Gleap$1.openChecklist(options.checklistId, options.showBackButton);
|
|
2594
|
+
return { opened: true };
|
|
2595
|
+
}
|
|
2596
|
+
async startChecklist(options) {
|
|
2597
|
+
Gleap$1.startChecklist(options.outboundId, options.showBackButton);
|
|
2598
|
+
return { opened: true };
|
|
2599
|
+
}
|
|
347
2600
|
async openFeatureRequests(options) {
|
|
348
2601
|
Gleap$1.openFeatureRequests(options.showBackButton);
|
|
349
2602
|
return { openedFeatureRequests: true };
|