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.
Files changed (43) hide show
  1. package/CapacitorGleapPlugin.podspec +7 -2
  2. package/Package.swift +31 -0
  3. package/README.md +247 -759
  4. package/android/build.gradle +1 -1
  5. package/android/src/main/java/io/gleapplugins/capacitor/GleapPlugin.java +184 -37
  6. package/dist/docs.json +267 -3893
  7. package/dist/esm/definitions.d.ts +145 -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 +37 -2
  33. package/dist/esm/web.js +34 -0
  34. package/dist/esm/web.js.map +1 -1
  35. package/dist/plugin.cjs.js +2254 -1
  36. package/dist/plugin.cjs.js.map +1 -1
  37. package/dist/plugin.js +2254 -1
  38. package/dist/plugin.js.map +1 -1
  39. package/ios/{Plugin → Sources/GleapPlugin}/GleapPlugin.swift +195 -12
  40. package/package.json +18 -4
  41. package/ios/Plugin/GleapPlugin.h +0 -10
  42. package/ios/Plugin/GleapPlugin.m +0 -68
  43. package/ios/Plugin/Info.plist +0 -24
@@ -0,0 +1,303 @@
1
+ /** Maximum captured size per body (request payload or response text). */
2
+ export const MAX_BODY_SIZE = 150000;
3
+ export const BINARY_BODY_OMITTED = '[binary body omitted]';
4
+ export const STREAMING_BODY_OMITTED = '[streaming body omitted]';
5
+ export const BODY_NOT_CAPTURED = '[body not captured]';
6
+ export const BODY_PENDING = '[body pending]';
7
+ const STREAMING_TYPES = [
8
+ 'text/event-stream',
9
+ 'application/x-ndjson',
10
+ 'application/stream+json',
11
+ 'multipart/x-mixed-replace',
12
+ 'grpc',
13
+ ];
14
+ const TEXT_TYPES = ['json', 'xml', 'text/', 'javascript', 'x-www-form-urlencoded', 'graphql'];
15
+ export function classifyContentType(contentType) {
16
+ if (typeof contentType !== 'string' || contentType.trim() === '') {
17
+ return 'unknown';
18
+ }
19
+ const lower = contentType.toLowerCase();
20
+ for (const type of STREAMING_TYPES) {
21
+ if (lower.indexOf(type) > -1) {
22
+ return 'streaming';
23
+ }
24
+ }
25
+ for (const type of TEXT_TYPES) {
26
+ if (lower.indexOf(type) > -1) {
27
+ return 'text';
28
+ }
29
+ }
30
+ return 'binary';
31
+ }
32
+ export function markerForKind(kind) {
33
+ if (kind === 'streaming') {
34
+ return STREAMING_BODY_OMITTED;
35
+ }
36
+ if (kind === 'binary') {
37
+ return BINARY_BODY_OMITTED;
38
+ }
39
+ return null;
40
+ }
41
+ /** UTF-8 byte length without allocating an encoded copy. */
42
+ export function utf8ByteLength(text) {
43
+ let bytes = 0;
44
+ for (let i = 0; i < text.length; i++) {
45
+ const code = text.charCodeAt(i);
46
+ if (code < 0x80) {
47
+ bytes += 1;
48
+ }
49
+ else if (code < 0x800) {
50
+ bytes += 2;
51
+ }
52
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
53
+ bytes += 4;
54
+ i++;
55
+ }
56
+ else {
57
+ bytes += 3;
58
+ }
59
+ }
60
+ return bytes;
61
+ }
62
+ /** Cuts a string at max chars without splitting a surrogate pair. */
63
+ export function sliceHead(text, max) {
64
+ if (text.length <= max) {
65
+ return text;
66
+ }
67
+ let end = max;
68
+ const last = text.charCodeAt(end - 1);
69
+ if (last >= 0xd800 && last <= 0xdbff) {
70
+ end--;
71
+ }
72
+ return text.slice(0, end);
73
+ }
74
+ export function truncationMarker(totalBytes) {
75
+ return `\n… [truncated, ${totalBytes !== null && totalBytes > MAX_BODY_SIZE ? totalBytes : `more than ${MAX_BODY_SIZE}`} bytes]`;
76
+ }
77
+ /** Caps a text body at MAX_BODY_SIZE chars: keeps the head and appends the truncation marker. */
78
+ export function capBodyText(text) {
79
+ if (text.length <= MAX_BODY_SIZE) {
80
+ return text;
81
+ }
82
+ return sliceHead(text, MAX_BODY_SIZE) + truncationMarker(utf8ByteLength(text));
83
+ }
84
+ /**
85
+ * Decodes bytes as UTF-8. With fatal (unknown content type) invalid UTF-8 yields null, so the caller
86
+ * can mark the body as binary. A cut-off (incomplete) body keeps a split trailing character pending
87
+ * instead of failing on it.
88
+ */
89
+ export function decodeUtf8(bytes, fatal, complete) {
90
+ if (typeof TextDecoder === 'undefined') {
91
+ return null;
92
+ }
93
+ try {
94
+ return new TextDecoder('utf-8', { fatal }).decode(bytes, {
95
+ stream: !complete,
96
+ });
97
+ }
98
+ catch (e) {
99
+ return null;
100
+ }
101
+ }
102
+ /**
103
+ * Turns (at most MAX_BODY_SIZE) bytes into the logged text: decoded, capped, marked as truncated
104
+ * when the body was longer, or the binary marker when it is not UTF-8 text.
105
+ */
106
+ export function bytesToBodyText(bytes, kind, complete, totalBytes) {
107
+ const head = bytes.byteLength > MAX_BODY_SIZE ? bytes.subarray(0, MAX_BODY_SIZE) : bytes;
108
+ const cut = !complete || bytes.byteLength > MAX_BODY_SIZE;
109
+ const text = decodeUtf8(head, kind === 'unknown', !cut);
110
+ if (text === null) {
111
+ return kind === 'unknown' ? BINARY_BODY_OMITTED : BODY_NOT_CAPTURED;
112
+ }
113
+ return cut ? text + truncationMarker(totalBytes) : text;
114
+ }
115
+ /**
116
+ * Reads at most MAX_BODY_SIZE bytes (plus the rest of the chunk that crossed it) and cancels the
117
+ * stream after that, so an endless or huge body is never read into memory.
118
+ */
119
+ export async function readStreamHead(stream) {
120
+ const reader = stream.getReader();
121
+ const chunks = [];
122
+ let received = 0;
123
+ let complete = false;
124
+ try {
125
+ while (received <= MAX_BODY_SIZE) {
126
+ const result = await reader.read();
127
+ if (result.done) {
128
+ complete = true;
129
+ break;
130
+ }
131
+ const chunk = result.value;
132
+ if (chunk && chunk.byteLength > 0) {
133
+ chunks.push(chunk);
134
+ received += chunk.byteLength;
135
+ }
136
+ }
137
+ }
138
+ finally {
139
+ if (!complete) {
140
+ try {
141
+ reader.cancel().catch(() => undefined);
142
+ }
143
+ catch (e) {
144
+ // Ignore.
145
+ }
146
+ }
147
+ }
148
+ const bytes = new Uint8Array(Math.min(received, MAX_BODY_SIZE + 1));
149
+ let offset = 0;
150
+ for (const chunk of chunks) {
151
+ if (offset >= bytes.byteLength) {
152
+ break;
153
+ }
154
+ const part = chunk.subarray(0, bytes.byteLength - offset);
155
+ bytes.set(part, offset);
156
+ offset += part.byteLength;
157
+ }
158
+ return { bytes, complete };
159
+ }
160
+ /** Reads the head of a blob (never the whole blob) and turns it into the logged text. */
161
+ export async function readBlobHead(blob, kind) {
162
+ const head = blob.slice(0, MAX_BODY_SIZE);
163
+ if (typeof head.arrayBuffer !== 'function') {
164
+ return BODY_NOT_CAPTURED;
165
+ }
166
+ const buffer = await head.arrayBuffer();
167
+ const complete = blob.size <= MAX_BODY_SIZE;
168
+ return bytesToBodyText(new Uint8Array(buffer), kind, complete, complete ? null : blob.size);
169
+ }
170
+ export function isBlob(value) {
171
+ return typeof Blob !== 'undefined' && value instanceof Blob;
172
+ }
173
+ export function isFormData(value) {
174
+ return typeof FormData !== 'undefined' && value instanceof FormData;
175
+ }
176
+ export function isUrlSearchParams(value) {
177
+ return typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams;
178
+ }
179
+ export function isReadableStream(value) {
180
+ return typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;
181
+ }
182
+ export function isDocument(value) {
183
+ return typeof Document !== 'undefined' && value instanceof Document;
184
+ }
185
+ /** Byte view of an ArrayBuffer or ArrayBufferView, or null for anything else. */
186
+ export function toBytes(value) {
187
+ if (typeof ArrayBuffer === 'undefined') {
188
+ return null;
189
+ }
190
+ if (value instanceof ArrayBuffer) {
191
+ return new Uint8Array(value);
192
+ }
193
+ if (ArrayBuffer.isView(value)) {
194
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
195
+ }
196
+ return null;
197
+ }
198
+ /**
199
+ * The logged request payload for a fetch/XHR body. Strings and buffers are captured synchronously;
200
+ * blobs are read asynchronously (head only). Form data, streams and documents are not captured.
201
+ */
202
+ export function captureRequestBody(body, kind) {
203
+ if (body === undefined || body === null) {
204
+ return '';
205
+ }
206
+ if (isReadableStream(body)) {
207
+ return STREAMING_BODY_OMITTED;
208
+ }
209
+ const marker = markerForKind(kind);
210
+ if (marker) {
211
+ return marker;
212
+ }
213
+ if (typeof body === 'string') {
214
+ return capBodyText(body);
215
+ }
216
+ if (isUrlSearchParams(body)) {
217
+ return capBodyText(body.toString());
218
+ }
219
+ if (isFormData(body) || isDocument(body)) {
220
+ return BODY_NOT_CAPTURED;
221
+ }
222
+ if (isBlob(body)) {
223
+ const blobKind = kind === 'unknown' ? classifyContentType(body.type) : kind;
224
+ const blobMarker = markerForKind(blobKind);
225
+ if (blobMarker) {
226
+ return blobMarker;
227
+ }
228
+ return readBlobHead(body, blobKind).catch(() => BODY_NOT_CAPTURED);
229
+ }
230
+ const bytes = toBytes(body);
231
+ if (bytes) {
232
+ return bytesToBodyText(bytes, kind, bytes.byteLength <= MAX_BODY_SIZE, bytes.byteLength);
233
+ }
234
+ try {
235
+ return capBodyText(String(body));
236
+ }
237
+ catch (e) {
238
+ return BODY_NOT_CAPTURED;
239
+ }
240
+ }
241
+ /**
242
+ * JSON.stringify for a parsed JSON value (XHR responseType "json") that stops once the output
243
+ * exceeds MAX_BODY_SIZE, so a huge response is not serialised in full.
244
+ */
245
+ export function stringifyJsonHead(value) {
246
+ const parts = [];
247
+ let length = 0;
248
+ let full = false;
249
+ const push = (text) => {
250
+ if (full) {
251
+ return;
252
+ }
253
+ parts.push(text);
254
+ length += text.length;
255
+ if (length > MAX_BODY_SIZE) {
256
+ full = true;
257
+ }
258
+ };
259
+ const write = (node) => {
260
+ if (full) {
261
+ return;
262
+ }
263
+ if (node === null || typeof node !== 'object') {
264
+ const text = JSON.stringify(node);
265
+ push(text === undefined ? 'null' : text);
266
+ return;
267
+ }
268
+ if (Array.isArray(node)) {
269
+ push('[');
270
+ for (let i = 0; i < node.length && !full; i++) {
271
+ if (i > 0) {
272
+ push(',');
273
+ }
274
+ write(node[i]);
275
+ }
276
+ push(']');
277
+ return;
278
+ }
279
+ push('{');
280
+ let first = true;
281
+ const record = node;
282
+ for (const key of Object.keys(record)) {
283
+ if (full) {
284
+ break;
285
+ }
286
+ const child = record[key];
287
+ if (child === undefined || typeof child === 'function') {
288
+ continue;
289
+ }
290
+ push(`${first ? '' : ','}${JSON.stringify(key)}:`);
291
+ first = false;
292
+ write(child);
293
+ }
294
+ push('}');
295
+ };
296
+ write(value);
297
+ const text = parts.join('');
298
+ if (!full) {
299
+ return text;
300
+ }
301
+ return sliceHead(text, MAX_BODY_SIZE) + truncationMarker(null);
302
+ }
303
+ //# sourceMappingURL=body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../../../src/logs/body.ts"],"names":[],"mappings":"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","sourcesContent":["/** Maximum captured size per body (request payload or response text). */\nexport const MAX_BODY_SIZE = 150000;\n\nexport const BINARY_BODY_OMITTED = '[binary body omitted]';\nexport const STREAMING_BODY_OMITTED = '[streaming body omitted]';\nexport const BODY_NOT_CAPTURED = '[body not captured]';\nexport const BODY_PENDING = '[body pending]';\n\n/**\n * text: capture; unknown (no content type): capture when it decodes as UTF-8;\n * streaming / binary: never read, only a marker.\n */\nexport type BodyKind = 'text' | 'unknown' | 'streaming' | 'binary';\n\nconst STREAMING_TYPES = [\n 'text/event-stream',\n 'application/x-ndjson',\n 'application/stream+json',\n 'multipart/x-mixed-replace',\n 'grpc',\n];\nconst TEXT_TYPES = ['json', 'xml', 'text/', 'javascript', 'x-www-form-urlencoded', 'graphql'];\n\nexport function classifyContentType(contentType: unknown): BodyKind {\n if (typeof contentType !== 'string' || contentType.trim() === '') {\n return 'unknown';\n }\n const lower = contentType.toLowerCase();\n for (const type of STREAMING_TYPES) {\n if (lower.indexOf(type) > -1) {\n return 'streaming';\n }\n }\n for (const type of TEXT_TYPES) {\n if (lower.indexOf(type) > -1) {\n return 'text';\n }\n }\n return 'binary';\n}\n\nexport function markerForKind(kind: BodyKind): string | null {\n if (kind === 'streaming') {\n return STREAMING_BODY_OMITTED;\n }\n if (kind === 'binary') {\n return BINARY_BODY_OMITTED;\n }\n return null;\n}\n\n/** UTF-8 byte length without allocating an encoded copy. */\nexport function utf8ByteLength(text: string): number {\n let bytes = 0;\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (code < 0x80) {\n bytes += 1;\n } else if (code < 0x800) {\n bytes += 2;\n } else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {\n bytes += 4;\n i++;\n } else {\n bytes += 3;\n }\n }\n return bytes;\n}\n\n/** Cuts a string at max chars without splitting a surrogate pair. */\nexport function sliceHead(text: string, max: number): string {\n if (text.length <= max) {\n return text;\n }\n let end = max;\n const last = text.charCodeAt(end - 1);\n if (last >= 0xd800 && last <= 0xdbff) {\n end--;\n }\n return text.slice(0, end);\n}\n\nexport function truncationMarker(totalBytes: number | null): string {\n return `\\n… [truncated, ${\n totalBytes !== null && totalBytes > MAX_BODY_SIZE ? totalBytes : `more than ${MAX_BODY_SIZE}`\n } bytes]`;\n}\n\n/** Caps a text body at MAX_BODY_SIZE chars: keeps the head and appends the truncation marker. */\nexport function capBodyText(text: string): string {\n if (text.length <= MAX_BODY_SIZE) {\n return text;\n }\n return sliceHead(text, MAX_BODY_SIZE) + truncationMarker(utf8ByteLength(text));\n}\n\n/**\n * Decodes bytes as UTF-8. With fatal (unknown content type) invalid UTF-8 yields null, so the caller\n * can mark the body as binary. A cut-off (incomplete) body keeps a split trailing character pending\n * instead of failing on it.\n */\nexport function decodeUtf8(bytes: Uint8Array, fatal: boolean, complete: boolean): string | null {\n if (typeof TextDecoder === 'undefined') {\n return null;\n }\n try {\n return new TextDecoder('utf-8', { fatal }).decode(bytes, {\n stream: !complete,\n });\n } catch (e) {\n return null;\n }\n}\n\n/**\n * Turns (at most MAX_BODY_SIZE) bytes into the logged text: decoded, capped, marked as truncated\n * when the body was longer, or the binary marker when it is not UTF-8 text.\n */\nexport function bytesToBodyText(\n bytes: Uint8Array,\n kind: BodyKind,\n complete: boolean,\n totalBytes: number | null,\n): string {\n const head = bytes.byteLength > MAX_BODY_SIZE ? bytes.subarray(0, MAX_BODY_SIZE) : bytes;\n const cut = !complete || bytes.byteLength > MAX_BODY_SIZE;\n const text = decodeUtf8(head, kind === 'unknown', !cut);\n if (text === null) {\n return kind === 'unknown' ? BINARY_BODY_OMITTED : BODY_NOT_CAPTURED;\n }\n return cut ? text + truncationMarker(totalBytes) : text;\n}\n\n/**\n * Reads at most MAX_BODY_SIZE bytes (plus the rest of the chunk that crossed it) and cancels the\n * stream after that, so an endless or huge body is never read into memory.\n */\nexport async function readStreamHead(\n stream: ReadableStream<Uint8Array>,\n): Promise<{ bytes: Uint8Array; complete: boolean }> {\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n let complete = false;\n try {\n while (received <= MAX_BODY_SIZE) {\n const result = await reader.read();\n if (result.done) {\n complete = true;\n break;\n }\n const chunk = result.value;\n if (chunk && chunk.byteLength > 0) {\n chunks.push(chunk);\n received += chunk.byteLength;\n }\n }\n } finally {\n if (!complete) {\n try {\n reader.cancel().catch(() => undefined);\n } catch (e) {\n // Ignore.\n }\n }\n }\n\n const bytes = new Uint8Array(Math.min(received, MAX_BODY_SIZE + 1));\n let offset = 0;\n for (const chunk of chunks) {\n if (offset >= bytes.byteLength) {\n break;\n }\n const part = chunk.subarray(0, bytes.byteLength - offset);\n bytes.set(part, offset);\n offset += part.byteLength;\n }\n return { bytes, complete };\n}\n\n/** Reads the head of a blob (never the whole blob) and turns it into the logged text. */\nexport async function readBlobHead(blob: Blob, kind: BodyKind): Promise<string> {\n const head = blob.slice(0, MAX_BODY_SIZE);\n if (typeof head.arrayBuffer !== 'function') {\n return BODY_NOT_CAPTURED;\n }\n const buffer = await head.arrayBuffer();\n const complete = blob.size <= MAX_BODY_SIZE;\n return bytesToBodyText(new Uint8Array(buffer), kind, complete, complete ? null : blob.size);\n}\n\nexport function isBlob(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob;\n}\n\nexport function isFormData(value: unknown): boolean {\n return typeof FormData !== 'undefined' && value instanceof FormData;\n}\n\nexport function isUrlSearchParams(value: unknown): value is URLSearchParams {\n return typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams;\n}\n\nexport function isReadableStream(value: unknown): boolean {\n return typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;\n}\n\nexport function isDocument(value: unknown): boolean {\n return typeof Document !== 'undefined' && value instanceof Document;\n}\n\n/** Byte view of an ArrayBuffer or ArrayBufferView, or null for anything else. */\nexport function toBytes(value: unknown): Uint8Array | null {\n if (typeof ArrayBuffer === 'undefined') {\n return null;\n }\n if (value instanceof ArrayBuffer) {\n return new Uint8Array(value);\n }\n if (ArrayBuffer.isView(value)) {\n return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n }\n return null;\n}\n\n/**\n * The logged request payload for a fetch/XHR body. Strings and buffers are captured synchronously;\n * blobs are read asynchronously (head only). Form data, streams and documents are not captured.\n */\nexport function captureRequestBody(body: unknown, kind: BodyKind): string | Promise<string> {\n if (body === undefined || body === null) {\n return '';\n }\n if (isReadableStream(body)) {\n return STREAMING_BODY_OMITTED;\n }\n const marker = markerForKind(kind);\n if (marker) {\n return marker;\n }\n if (typeof body === 'string') {\n return capBodyText(body);\n }\n if (isUrlSearchParams(body)) {\n return capBodyText(body.toString());\n }\n if (isFormData(body) || isDocument(body)) {\n return BODY_NOT_CAPTURED;\n }\n if (isBlob(body)) {\n const blobKind = kind === 'unknown' ? classifyContentType(body.type) : kind;\n const blobMarker = markerForKind(blobKind);\n if (blobMarker) {\n return blobMarker;\n }\n return readBlobHead(body, blobKind).catch(() => BODY_NOT_CAPTURED);\n }\n const bytes = toBytes(body);\n if (bytes) {\n return bytesToBodyText(bytes, kind, bytes.byteLength <= MAX_BODY_SIZE, bytes.byteLength);\n }\n try {\n return capBodyText(String(body));\n } catch (e) {\n return BODY_NOT_CAPTURED;\n }\n}\n\n/**\n * JSON.stringify for a parsed JSON value (XHR responseType \"json\") that stops once the output\n * exceeds MAX_BODY_SIZE, so a huge response is not serialised in full.\n */\nexport function stringifyJsonHead(value: unknown): string {\n const parts: string[] = [];\n let length = 0;\n let full = false;\n const push = (text: string) => {\n if (full) {\n return;\n }\n parts.push(text);\n length += text.length;\n if (length > MAX_BODY_SIZE) {\n full = true;\n }\n };\n const write = (node: unknown): void => {\n if (full) {\n return;\n }\n if (node === null || typeof node !== 'object') {\n const text = JSON.stringify(node);\n push(text === undefined ? 'null' : text);\n return;\n }\n if (Array.isArray(node)) {\n push('[');\n for (let i = 0; i < node.length && !full; i++) {\n if (i > 0) {\n push(',');\n }\n write(node[i]);\n }\n push(']');\n return;\n }\n push('{');\n let first = true;\n const record = node as { [key: string]: unknown };\n for (const key of Object.keys(record)) {\n if (full) {\n break;\n }\n const child = record[key];\n if (child === undefined || typeof child === 'function') {\n continue;\n }\n push(`${first ? '' : ','}${JSON.stringify(key)}:`);\n first = false;\n write(child);\n }\n push('}');\n };\n write(value);\n const text = parts.join('');\n if (!full) {\n return text;\n }\n return sliceHead(text, MAX_BODY_SIZE) + truncationMarker(null);\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import type { GleapConsoleLogEntry } from '../definitions';
2
+ export declare const MAX_CONSOLE_ENTRIES = 500;
3
+ /**
4
+ * Records the WebView console. Methods are patched in place on the existing console object (never
5
+ * replaced) and always call through to what was there before.
6
+ */
7
+ export declare class ConsoleCapture {
8
+ private readonly win;
9
+ private readonly onChange;
10
+ private entries;
11
+ private patched;
12
+ private active;
13
+ private isRecording;
14
+ private errorListener;
15
+ private rejectionListener;
16
+ constructor(win: Window, onChange: () => void);
17
+ install(): void;
18
+ /** Restores the console methods that are still ours, stops listening and drops what was recorded. */
19
+ uninstall(): void;
20
+ getEntries(): GleapConsoleLogEntry[];
21
+ private record;
22
+ /** Formats and records; re-entrant calls (a getter that logs while being formatted) are dropped. */
23
+ private recordArgs;
24
+ private patchConsole;
25
+ private patchMethod;
26
+ private listenForUncaughtErrors;
27
+ private recordUncaughtError;
28
+ private recordGuarded;
29
+ }
@@ -0,0 +1,189 @@
1
+ import { MAX_ERROR_LOG_LENGTH, MAX_LOG_LENGTH, formatConsoleArgs, formatError, formatValue, isError, truncateLog, } from './format';
2
+ export const MAX_CONSOLE_ENTRIES = 500;
3
+ const PATCHED_METHODS = {
4
+ log: 'INFO',
5
+ info: 'INFO',
6
+ debug: 'INFO',
7
+ warn: 'WARNING',
8
+ error: 'ERROR',
9
+ };
10
+ /**
11
+ * Records the WebView console. Methods are patched in place on the existing console object (never
12
+ * replaced) and always call through to what was there before.
13
+ */
14
+ export class ConsoleCapture {
15
+ constructor(win, onChange) {
16
+ this.win = win;
17
+ this.onChange = onChange;
18
+ this.entries = [];
19
+ this.patched = [];
20
+ this.active = false;
21
+ this.isRecording = false;
22
+ this.errorListener = null;
23
+ this.rejectionListener = null;
24
+ }
25
+ install() {
26
+ if (this.active) {
27
+ return;
28
+ }
29
+ this.active = true;
30
+ this.patchConsole();
31
+ this.listenForUncaughtErrors();
32
+ }
33
+ /** Restores the console methods that are still ours, stops listening and drops what was recorded. */
34
+ uninstall() {
35
+ this.active = false;
36
+ const console = this.win.console;
37
+ for (const method of this.patched) {
38
+ try {
39
+ if (console[method.name] === method.wrapper) {
40
+ console[method.name] = method.original;
41
+ }
42
+ }
43
+ catch (e) {
44
+ // The inactive wrapper keeps calling through.
45
+ }
46
+ }
47
+ this.patched = [];
48
+ if (this.errorListener) {
49
+ this.win.removeEventListener('error', this.errorListener);
50
+ this.errorListener = null;
51
+ }
52
+ if (this.rejectionListener) {
53
+ this.win.removeEventListener('unhandledrejection', this.rejectionListener);
54
+ this.rejectionListener = null;
55
+ }
56
+ this.entries = [];
57
+ }
58
+ getEntries() {
59
+ return this.entries.slice();
60
+ }
61
+ record(priority, text) {
62
+ if (!this.active || !text) {
63
+ return;
64
+ }
65
+ this.entries.push({
66
+ date: new Date().toISOString(),
67
+ priority,
68
+ log: truncateLog(text, priority === 'ERROR' ? MAX_ERROR_LOG_LENGTH : MAX_LOG_LENGTH),
69
+ });
70
+ if (this.entries.length > MAX_CONSOLE_ENTRIES) {
71
+ this.entries.splice(0, this.entries.length - MAX_CONSOLE_ENTRIES);
72
+ }
73
+ this.onChange();
74
+ }
75
+ /** Formats and records; re-entrant calls (a getter that logs while being formatted) are dropped. */
76
+ recordArgs(priority, args, prefix = '') {
77
+ if (!this.active || this.isRecording) {
78
+ return;
79
+ }
80
+ this.isRecording = true;
81
+ try {
82
+ const budget = priority === 'ERROR' ? MAX_ERROR_LOG_LENGTH : MAX_LOG_LENGTH;
83
+ const text = formatConsoleArgs(args, budget);
84
+ this.record(priority, prefix ? (text ? `${prefix} ${text}` : prefix) : text);
85
+ }
86
+ catch (e) {
87
+ // Logging must never break the app.
88
+ }
89
+ finally {
90
+ this.isRecording = false;
91
+ }
92
+ }
93
+ patchConsole() {
94
+ const console = this.win.console;
95
+ if (!console) {
96
+ return;
97
+ }
98
+ const capture = this;
99
+ for (const name of Object.keys(PATCHED_METHODS)) {
100
+ const priority = PATCHED_METHODS[name];
101
+ this.patchMethod(console, name, function (original, args) {
102
+ capture.recordArgs(priority, args);
103
+ return original.apply(this, args);
104
+ });
105
+ }
106
+ this.patchMethod(console, 'assert', function (original, args) {
107
+ if (!args[0]) {
108
+ capture.recordArgs('ERROR', args.slice(1), 'Assertion failed:');
109
+ }
110
+ return original.apply(this, args);
111
+ });
112
+ }
113
+ patchMethod(console, name, body) {
114
+ const original = console[name];
115
+ if (typeof original !== 'function') {
116
+ return;
117
+ }
118
+ const target = console;
119
+ const wrapper = function (...args) {
120
+ return body.call(this === undefined ? target : this, original, args);
121
+ };
122
+ try {
123
+ console[name] = wrapper;
124
+ }
125
+ catch (e) {
126
+ return;
127
+ }
128
+ if (console[name] === wrapper) {
129
+ this.patched.push({
130
+ name,
131
+ original: original,
132
+ wrapper,
133
+ });
134
+ }
135
+ }
136
+ listenForUncaughtErrors() {
137
+ this.errorListener = (event) => {
138
+ try {
139
+ const errorEvent = event;
140
+ if (typeof ErrorEvent !== 'undefined' && !(event instanceof ErrorEvent) && !errorEvent.error) {
141
+ // Resource load errors are plain Events.
142
+ return;
143
+ }
144
+ this.recordUncaughtError(errorEvent);
145
+ }
146
+ catch (e) {
147
+ // Ignore.
148
+ }
149
+ };
150
+ this.rejectionListener = (event) => {
151
+ try {
152
+ const reason = event.reason;
153
+ const text = isError(reason) ? formatError(reason) : formatValue(reason, MAX_ERROR_LOG_LENGTH);
154
+ this.recordGuarded(`Unhandled promise rejection: ${text}`);
155
+ }
156
+ catch (e) {
157
+ // Ignore.
158
+ }
159
+ };
160
+ this.win.addEventListener('error', this.errorListener);
161
+ this.win.addEventListener('unhandledrejection', this.rejectionListener);
162
+ }
163
+ recordUncaughtError(event) {
164
+ let text;
165
+ if (event.error && isError(event.error)) {
166
+ text = formatError(event.error);
167
+ }
168
+ else {
169
+ text = event.message || formatValue(event.error, MAX_ERROR_LOG_LENGTH);
170
+ if (event.filename) {
171
+ text += ` (${event.filename}:${event.lineno || 0}:${event.colno || 0})`;
172
+ }
173
+ }
174
+ this.recordGuarded(text.indexOf('Uncaught') === 0 ? text : `Uncaught ${text}`);
175
+ }
176
+ recordGuarded(text) {
177
+ if (this.isRecording) {
178
+ return;
179
+ }
180
+ this.isRecording = true;
181
+ try {
182
+ this.record('ERROR', text);
183
+ }
184
+ finally {
185
+ this.isRecording = false;
186
+ }
187
+ }
188
+ }
189
+ //# sourceMappingURL=consoleCapture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consoleCapture.js","sourceRoot":"","sources":["../../../src/logs/consoleCapture.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,OAAO,EACP,WAAW,GACZ,MAAM,UAAU,CAAC;AAElB,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAIvC,MAAM,eAAe,GAAmC;IACtD,GAAG,EAAE,MAAM;IACX,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,OAAO;CACf,CAAC;AAQF;;;GAGG;AACH,MAAM,OAAO,cAAc;IAQzB,YACmB,GAAW,EACX,QAAoB;QADpB,QAAG,GAAH,GAAG,CAAQ;QACX,aAAQ,GAAR,QAAQ,CAAY;QAT/B,YAAO,GAA2B,EAAE,CAAC;QACrC,YAAO,GAAoB,EAAE,CAAC;QAC9B,WAAM,GAAG,KAAK,CAAC;QACf,gBAAW,GAAG,KAAK,CAAC;QACpB,kBAAa,GAAoC,IAAI,CAAC;QACtD,sBAAiB,GAAoC,IAAI,CAAC;IAK/D,CAAC;IAEJ,OAAO;QACL,IAAI,IAAI,CAAC,MAAM,EAAE;YACf,OAAO;SACR;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,uBAAuB,EAAE,CAAC;IACjC,CAAC;IAED,qGAAqG;IACrG,SAAS;QACP,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,MAAM,OAAO,GAAI,IAAI,CAAC,GAAuC,CAAC,OAAwC,CAAC;QACvG,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE;YACjC,IAAI;gBACF,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,OAAO,EAAE;oBAC3C,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC;iBACxC;aACF;YAAC,OAAO,CAAC,EAAE;gBACV,8CAA8C;aAC/C;SACF;QACD,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,IAAI,CAAC,aAAa,EAAE;YACtB,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;YAC1D,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;SAC3B;QACD,IAAI,IAAI,CAAC,iBAAiB,EAAE;YAC1B,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC,oBAAoB,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;YAC3E,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;SAC/B;QACD,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACpB,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;IAEO,MAAM,CAAC,QAAkB,EAAE,IAAY;QAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE;YACzB,OAAO;SACR;QACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,QAAQ;YACR,GAAG,EAAE,WAAW,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,cAAc,CAAC;SACrF,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,mBAAmB,EAAE;YAC7C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC;SACnE;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC;IAED,oGAAoG;IAC5F,UAAU,CAAC,QAAkB,EAAE,IAAwB,EAAE,MAAM,GAAG,EAAE;QAC1E,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,WAAW,EAAE;YACpC,OAAO;SACR;QACD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI;YACF,MAAM,MAAM,GAAG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,cAAc,CAAC;YAC5E,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAC9E;QAAC,OAAO,CAAC,EAAE;YACV,oCAAoC;SACrC;gBAAS;YACR,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;SAC1B;IACH,CAAC;IAEO,YAAY;QAClB,MAAM,OAAO,GAAI,IAAI,CAAC,GAAuC,CAAC,OAAwC,CAAC;QACvG,IAAI,CAAC,OAAO,EAAE;YACZ,OAAO;SACR;QACD,MAAM,OAAO,GAAG,IAAI,CAAC;QACrB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE;YAC/C,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,WAAW,CACd,OAAO,EACP,IAAI,EACJ,UAAyB,QAAyC,EAAE,IAAe;gBACjF,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;gBACnC,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACpC,CAAC,CACF,CAAC;SACH;QACD,IAAI,CAAC,WAAW,CACd,OAAO,EACP,QAAQ,EACR,UAAyB,QAAyC,EAAE,IAAe;YACjF,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;gBACZ,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;aACjE;YACD,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,CAAC,CACF,CAAC;IACJ,CAAC;IAEO,WAAW,CACjB,OAAsC,EACtC,IAAY,EACZ,IAA4F;QAE5F,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE;YAClC,OAAO;SACR;QACD,MAAM,MAAM,GAAG,OAAO,CAAC;QACvB,MAAM,OAAO,GAAG,UAAyB,GAAG,IAAe;YACzD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,QAA2C,EAAE,IAAI,CAAC,CAAC;QAC1G,CAAC,CAAC;QACF,IAAI;YACF,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;SACzB;QAAC,OAAO,CAAC,EAAE;YACV,OAAO;SACR;QACD,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE;YAC7B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;gBAChB,IAAI;gBACJ,QAAQ,EAAE,QAA2C;gBACrD,OAAO;aACR,CAAC,CAAC;SACJ;IACH,CAAC;IAEO,uBAAuB;QAC7B,IAAI,CAAC,aAAa,GAAG,CAAC,KAAY,EAAE,EAAE;YACpC,IAAI;gBACF,MAAM,UAAU,GAAG,KAAmB,CAAC;gBACvC,IAAI,OAAO,UAAU,KAAK,WAAW,IAAI,CAAC,CAAC,KAAK,YAAY,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE;oBAC5F,yCAAyC;oBACzC,OAAO;iBACR;gBACD,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;aACtC;YAAC,OAAO,CAAC,EAAE;gBACV,UAAU;aACX;QACH,CAAC,CAAC;QACF,IAAI,CAAC,iBAAiB,GAAG,CAAC,KAAY,EAAE,EAAE;YACxC,IAAI;gBACF,MAAM,MAAM,GAAI,KAA+B,CAAC,MAAM,CAAC;gBACvD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;gBAC/F,IAAI,CAAC,aAAa,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;aAC5D;YAAC,OAAO,CAAC,EAAE;gBACV,UAAU;aACX;QACH,CAAC,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QACvD,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC1E,CAAC;IAEO,mBAAmB,CAAC,KAAiB;QAC3C,IAAI,IAAY,CAAC;QACjB,IAAI,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;YACvC,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;SACjC;aAAM;YACL,IAAI,GAAG,KAAK,CAAC,OAAO,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC;YACvE,IAAI,KAAK,CAAC,QAAQ,EAAE;gBAClB,IAAI,IAAI,KAAK,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC;aACzE;SACF;QACD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACjF,CAAC;IAEO,aAAa,CAAC,IAAY;QAChC,IAAI,IAAI,CAAC,WAAW,EAAE;YACpB,OAAO;SACR;QACD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI;YACF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;SAC5B;gBAAS;YACR,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;SAC1B;IACH,CAAC;CACF","sourcesContent":["import type { GleapConsoleLogEntry } from '../definitions';\n\nimport {\n MAX_ERROR_LOG_LENGTH,\n MAX_LOG_LENGTH,\n formatConsoleArgs,\n formatError,\n formatValue,\n isError,\n truncateLog,\n} from './format';\n\nexport const MAX_CONSOLE_ENTRIES = 500;\n\ntype Priority = GleapConsoleLogEntry['priority'];\n\nconst PATCHED_METHODS: { [method: string]: Priority } = {\n log: 'INFO',\n info: 'INFO',\n debug: 'INFO',\n warn: 'WARNING',\n error: 'ERROR',\n};\n\ninterface PatchedMethod {\n name: string;\n original: (...args: unknown[]) => unknown;\n wrapper: (...args: unknown[]) => unknown;\n}\n\n/**\n * Records the WebView console. Methods are patched in place on the existing console object (never\n * replaced) and always call through to what was there before.\n */\nexport class ConsoleCapture {\n private entries: GleapConsoleLogEntry[] = [];\n private patched: PatchedMethod[] = [];\n private active = false;\n private isRecording = false;\n private errorListener: ((event: Event) => void) | null = null;\n private rejectionListener: ((event: Event) => void) | null = null;\n\n constructor(\n private readonly win: Window,\n private readonly onChange: () => void,\n ) {}\n\n install(): void {\n if (this.active) {\n return;\n }\n this.active = true;\n this.patchConsole();\n this.listenForUncaughtErrors();\n }\n\n /** Restores the console methods that are still ours, stops listening and drops what was recorded. */\n uninstall(): void {\n this.active = false;\n const console = (this.win as unknown as { console: unknown }).console as { [method: string]: unknown };\n for (const method of this.patched) {\n try {\n if (console[method.name] === method.wrapper) {\n console[method.name] = method.original;\n }\n } catch (e) {\n // The inactive wrapper keeps calling through.\n }\n }\n this.patched = [];\n if (this.errorListener) {\n this.win.removeEventListener('error', this.errorListener);\n this.errorListener = null;\n }\n if (this.rejectionListener) {\n this.win.removeEventListener('unhandledrejection', this.rejectionListener);\n this.rejectionListener = null;\n }\n this.entries = [];\n }\n\n getEntries(): GleapConsoleLogEntry[] {\n return this.entries.slice();\n }\n\n private record(priority: Priority, text: string): void {\n if (!this.active || !text) {\n return;\n }\n this.entries.push({\n date: new Date().toISOString(),\n priority,\n log: truncateLog(text, priority === 'ERROR' ? MAX_ERROR_LOG_LENGTH : MAX_LOG_LENGTH),\n });\n if (this.entries.length > MAX_CONSOLE_ENTRIES) {\n this.entries.splice(0, this.entries.length - MAX_CONSOLE_ENTRIES);\n }\n this.onChange();\n }\n\n /** Formats and records; re-entrant calls (a getter that logs while being formatted) are dropped. */\n private recordArgs(priority: Priority, args: ArrayLike<unknown>, prefix = ''): void {\n if (!this.active || this.isRecording) {\n return;\n }\n this.isRecording = true;\n try {\n const budget = priority === 'ERROR' ? MAX_ERROR_LOG_LENGTH : MAX_LOG_LENGTH;\n const text = formatConsoleArgs(args, budget);\n this.record(priority, prefix ? (text ? `${prefix} ${text}` : prefix) : text);\n } catch (e) {\n // Logging must never break the app.\n } finally {\n this.isRecording = false;\n }\n }\n\n private patchConsole(): void {\n const console = (this.win as unknown as { console: unknown }).console as { [method: string]: unknown };\n if (!console) {\n return;\n }\n const capture = this;\n for (const name of Object.keys(PATCHED_METHODS)) {\n const priority = PATCHED_METHODS[name];\n this.patchMethod(\n console,\n name,\n function (this: unknown, original: (...args: unknown[]) => unknown, args: unknown[]) {\n capture.recordArgs(priority, args);\n return original.apply(this, args);\n },\n );\n }\n this.patchMethod(\n console,\n 'assert',\n function (this: unknown, original: (...args: unknown[]) => unknown, args: unknown[]) {\n if (!args[0]) {\n capture.recordArgs('ERROR', args.slice(1), 'Assertion failed:');\n }\n return original.apply(this, args);\n },\n );\n }\n\n private patchMethod(\n console: { [method: string]: unknown },\n name: string,\n body: (this: unknown, original: (...args: unknown[]) => unknown, args: unknown[]) => unknown,\n ): void {\n const original = console[name];\n if (typeof original !== 'function') {\n return;\n }\n const target = console;\n const wrapper = function (this: unknown, ...args: unknown[]): unknown {\n return body.call(this === undefined ? target : this, original as (...args: unknown[]) => unknown, args);\n };\n try {\n console[name] = wrapper;\n } catch (e) {\n return;\n }\n if (console[name] === wrapper) {\n this.patched.push({\n name,\n original: original as (...args: unknown[]) => unknown,\n wrapper,\n });\n }\n }\n\n private listenForUncaughtErrors(): void {\n this.errorListener = (event: Event) => {\n try {\n const errorEvent = event as ErrorEvent;\n if (typeof ErrorEvent !== 'undefined' && !(event instanceof ErrorEvent) && !errorEvent.error) {\n // Resource load errors are plain Events.\n return;\n }\n this.recordUncaughtError(errorEvent);\n } catch (e) {\n // Ignore.\n }\n };\n this.rejectionListener = (event: Event) => {\n try {\n const reason = (event as PromiseRejectionEvent).reason;\n const text = isError(reason) ? formatError(reason) : formatValue(reason, MAX_ERROR_LOG_LENGTH);\n this.recordGuarded(`Unhandled promise rejection: ${text}`);\n } catch (e) {\n // Ignore.\n }\n };\n this.win.addEventListener('error', this.errorListener);\n this.win.addEventListener('unhandledrejection', this.rejectionListener);\n }\n\n private recordUncaughtError(event: ErrorEvent): void {\n let text: string;\n if (event.error && isError(event.error)) {\n text = formatError(event.error);\n } else {\n text = event.message || formatValue(event.error, MAX_ERROR_LOG_LENGTH);\n if (event.filename) {\n text += ` (${event.filename}:${event.lineno || 0}:${event.colno || 0})`;\n }\n }\n this.recordGuarded(text.indexOf('Uncaught') === 0 ? text : `Uncaught ${text}`);\n }\n\n private recordGuarded(text: string): void {\n if (this.isRecording) {\n return;\n }\n this.isRecording = true;\n try {\n this.record('ERROR', text);\n } finally {\n this.isRecording = false;\n }\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ export declare const MAX_LOG_LENGTH = 1000;
2
+ export declare const MAX_ERROR_LOG_LENGTH = 5000;
3
+ export declare const TRUNCATED_SUFFIX = "\u2026 [truncated]";
4
+ /** Keeps the head of a log line so the result (incl. the suffix) fits max chars. */
5
+ export declare function truncateLog(text: string, max: number): string;
6
+ /**
7
+ * Formats console arguments like the browser console does (printf-style %s %d %i %f %o %O %c in
8
+ * the first string), with objects as depth-limited JSON. Stops early once the text exceeds the
9
+ * budget, so logging a huge object stays cheap.
10
+ */
11
+ export declare function formatConsoleArgs(args: ArrayLike<unknown>, budget: number): string;
12
+ /** A single value as text: strings as they are, errors with their stack, objects as JSON. */
13
+ export declare function formatValue(value: unknown, budget: number): string;
14
+ /** "Name: message" followed by the stack (V8 stacks already start with that line). */
15
+ export declare function formatError(error: unknown): string;
16
+ export declare function isError(value: unknown): boolean;