cuoral-ionic 0.0.1 → 0.0.3

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 (85) hide show
  1. package/README.md +81 -4
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/build/.transforms/2c48e1f34ca03014b78fcb3e0ab7197b/results.bin +1 -0
  11. package/android/build/.transforms/2c48e1f34ca03014b78fcb3e0ab7197b/transformed/classes/classes_dex/classes.dex +0 -0
  12. package/android/build/.transforms/980c51bd075f726f311ad662d5d20ba0/results.bin +1 -0
  13. package/android/build/.transforms/980c51bd075f726f311ad662d5d20ba0/transformed/classes/classes_dex/classes.dex +0 -0
  14. package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/results.bin +1 -0
  15. package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
  16. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/results.bin +1 -0
  17. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
  18. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
  19. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
  20. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/ScreenRecordService.dex +0 -0
  21. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
  22. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/AndroidManifest.xml +22 -0
  23. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/output-metadata.json +18 -0
  24. package/android/build/intermediates/aar_main_jar/debug/classes.jar +0 -0
  25. package/android/build/intermediates/aar_metadata/debug/aar-metadata.properties +6 -0
  26. package/android/build/intermediates/annotation_processor_list/debug/annotationProcessors.json +1 -0
  27. package/android/build/intermediates/annotations_typedef_file/debug/typedefs.txt +0 -0
  28. package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
  29. package/android/build/intermediates/compile_r_class_jar/debug/R.jar +0 -0
  30. package/android/build/intermediates/compile_symbol_list/debug/R.txt +0 -0
  31. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -0
  32. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  33. package/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state +0 -0
  34. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  35. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  36. package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +2 -0
  37. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  38. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  39. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
  40. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/ScreenRecordService.class +0 -0
  41. package/android/build/intermediates/local_only_symbol_list/debug/R-def.txt +2 -0
  42. package/android/build/intermediates/manifest_merge_blame_file/debug/manifest-merger-blame-debug-report.txt +38 -0
  43. package/android/build/intermediates/merged_java_res/debug/feature-cuoral-ionic.jar +0 -0
  44. package/android/build/intermediates/merged_manifest/debug/AndroidManifest.xml +22 -0
  45. package/android/build/intermediates/navigation_json/debug/navigation.json +1 -0
  46. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  47. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  48. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
  49. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/ScreenRecordService.class +0 -0
  50. package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
  51. package/android/build/intermediates/symbol_list_with_package_name/debug/package-aware-r.txt +1 -0
  52. package/android/build/outputs/aar/cuoral-ionic-debug.aar +0 -0
  53. package/android/build/outputs/logs/manifest-merger-debug-report.txt +49 -0
  54. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin$1.class.uniqueId1 +0 -0
  55. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin$2.class.uniqueId2 +0 -0
  56. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin.class.uniqueId0 +0 -0
  57. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  58. package/android/build.gradle +61 -0
  59. package/android/src/main/AndroidManifest.xml +9 -0
  60. package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +290 -33
  61. package/android/src/main/java/com/cuoral/ionic/ScreenRecordService.java +59 -0
  62. package/dist/cuoral.d.ts +30 -1
  63. package/dist/cuoral.d.ts.map +1 -1
  64. package/dist/cuoral.js +168 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.esm.js +706 -127
  68. package/dist/index.esm.js.map +1 -1
  69. package/dist/index.js +706 -126
  70. package/dist/index.js.map +1 -1
  71. package/dist/intelligence.d.ts +66 -0
  72. package/dist/intelligence.d.ts.map +1 -0
  73. package/dist/intelligence.js +508 -0
  74. package/dist/plugin.d.ts +1 -1
  75. package/dist/plugin.d.ts.map +1 -1
  76. package/dist/plugin.js +24 -6
  77. package/dist/web.d.ts.map +1 -1
  78. package/dist/web.js +0 -3
  79. package/ios/Plugin/CuoralPlugin.swift +78 -1
  80. package/package.json +1 -1
  81. package/src/cuoral.ts +205 -1
  82. package/src/index.ts +1 -0
  83. package/src/intelligence.ts +609 -0
  84. package/src/plugin.ts +26 -6
  85. package/src/web.ts +0 -6
@@ -0,0 +1,609 @@
1
+ import { Capacitor } from '@capacitor/core';
2
+ import { registerPlugin } from '@capacitor/core';
3
+
4
+ // Define the plugin interface
5
+ interface CuoralPluginInterface {
6
+ setupNativeErrorCapture(options: { backendUrl: string; sessionId: string }): Promise<{ success: boolean; message: string }>;
7
+ }
8
+
9
+ // Register the Cuoral plugin for native error capture
10
+ const CuoralPlugin = registerPlugin<CuoralPluginInterface>('CuoralPlugin');
11
+
12
+ interface IntelligenceConfig {
13
+ consoleErrorBackendUrl: string;
14
+ pageViewBackendUrl: string;
15
+ apiResponseBackendUrl: string;
16
+ batchSize: number;
17
+ batchInterval: number;
18
+ maxQueueSize: number;
19
+ retryAttempts: number;
20
+ retryDelay: number;
21
+ }
22
+
23
+ interface QueueEvent {
24
+ event_type: string;
25
+ timestamp: number;
26
+ session_id: string;
27
+ user_agent: string;
28
+ url: string;
29
+ data: any;
30
+ }
31
+
32
+ interface PageViewData {
33
+ screen: string;
34
+ referrer: string;
35
+ duration: number;
36
+ metadata?: any;
37
+ }
38
+
39
+ interface ConsoleErrorData {
40
+ message: string;
41
+ stack_trace: string;
42
+ log_level: string;
43
+ metadata?: any;
44
+ }
45
+
46
+ interface ApiCallData {
47
+ method: string;
48
+ url: string;
49
+ status_code: number;
50
+ response_data: any;
51
+ duration: number;
52
+ error?: string;
53
+ }
54
+
55
+ export class CuoralIntelligence {
56
+ private config: IntelligenceConfig = {
57
+ consoleErrorBackendUrl: 'https://api.cuoral.com/customer-intelligence/console-error',
58
+ pageViewBackendUrl: 'https://api.cuoral.com/customer-intelligence/page-view',
59
+ apiResponseBackendUrl: 'https://api.cuoral.com/customer-intelligence/api-response',
60
+ batchSize: 10,
61
+ batchInterval: 2000,
62
+ maxQueueSize: 30,
63
+ retryAttempts: 1,
64
+ retryDelay: 2000,
65
+ };
66
+
67
+ private queues: Record<string, QueueEvent[]> = {
68
+ console_error: [],
69
+ unhandled_error: [],
70
+ page_view: [],
71
+ api_call: [],
72
+ };
73
+
74
+ private batchTimers: Record<string, any> = {};
75
+ private sessionId: string | null = null;
76
+ private isInitialized = false;
77
+ private lastPageViewTimestamp = Date.now();
78
+ private lastPageViewScreen = '';
79
+ private pendingEvents: any[] = [];
80
+
81
+ // Network monitoring state
82
+ private originalFetch: any = null;
83
+ private originalXMLHttpRequest: any = null;
84
+
85
+ constructor(sessionId: string) {
86
+ this.sessionId = sessionId;
87
+ }
88
+
89
+ /**
90
+ * Initialize intelligence tracking
91
+ */
92
+ public init(): void {
93
+ if (this.isInitialized) {
94
+ console.warn('[Cuoral Intelligence] Already initialized');
95
+ return;
96
+ }
97
+
98
+ this.setupConsoleErrorListener();
99
+ this.setupNetworkMonitoring();
100
+ this.setupAppStateListener();
101
+ this.setupNativeErrorCapture();
102
+
103
+ this.isInitialized = true;
104
+ this.flushPendingEvents();
105
+ }
106
+
107
+ /**
108
+ * Track a page/screen view
109
+ */
110
+ public trackPageView(screen: string, metadata?: any): void {
111
+ const duration = Date.now() - this.lastPageViewTimestamp;
112
+
113
+ this.enqueueEvent('page_view', {
114
+ screen,
115
+ referrer: this.lastPageViewScreen,
116
+ duration,
117
+ metadata: metadata || {},
118
+ });
119
+
120
+ this.lastPageViewTimestamp = Date.now();
121
+ this.lastPageViewScreen = screen;
122
+ }
123
+
124
+ /**
125
+ * Track a console error manually
126
+ */
127
+ public trackError(message: string, stackTrace?: string, metadata?: any): void {
128
+ this.enqueueEvent('console_error', {
129
+ message,
130
+ stack_trace: stackTrace || 'No stack trace available',
131
+ log_level: 'error',
132
+ metadata: metadata || {},
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Flush all queued events immediately
138
+ */
139
+ public flush(): void {
140
+ Object.keys(this.queues).forEach((type) => {
141
+ this.flushQueue(type);
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Destroy and cleanup
147
+ */
148
+ public destroy(): void {
149
+ // Clear all timers
150
+ Object.keys(this.batchTimers).forEach((type) => {
151
+ if (this.batchTimers[type]) {
152
+ clearTimeout(this.batchTimers[type]);
153
+ delete this.batchTimers[type];
154
+ }
155
+ });
156
+
157
+ // Restore original functions
158
+ if (this.originalFetch) {
159
+ window.fetch = this.originalFetch;
160
+ this.originalFetch = null;
161
+ }
162
+
163
+ if (this.originalXMLHttpRequest) {
164
+ (window as any).XMLHttpRequest = this.originalXMLHttpRequest;
165
+ this.originalXMLHttpRequest = null;
166
+ }
167
+
168
+ // Clear queues
169
+ Object.keys(this.queues).forEach((type) => {
170
+ this.queues[type] = [];
171
+ });
172
+
173
+ this.isInitialized = false;
174
+ }
175
+
176
+ /**
177
+ * Setup console error listener
178
+ */
179
+ private setupConsoleErrorListener(): void {
180
+ // Override console.error
181
+ const originalConsoleError = console.error;
182
+ console.error = (...args: any[]) => {
183
+ originalConsoleError.apply(console, args);
184
+
185
+ try {
186
+ // Try to find an Error object in arguments to get real stack trace
187
+ let stack = '';
188
+ let message = '';
189
+
190
+ // Check each argument for error information
191
+ for (const arg of args) {
192
+ if (arg instanceof Error) {
193
+ stack = arg.stack || '';
194
+ message = arg.message;
195
+ break;
196
+ } else if (arg && typeof arg === 'object') {
197
+ if (arg.stack && typeof arg.stack === 'string') {
198
+ stack = arg.stack;
199
+ message = arg.message || arg.toString();
200
+ break;
201
+ }
202
+ if (arg.rejection && arg.rejection instanceof Error) {
203
+ stack = arg.rejection.stack || '';
204
+ message = arg.rejection.message;
205
+ break;
206
+ }
207
+ if (arg.error instanceof Error) {
208
+ stack = arg.error.stack || '';
209
+ message = arg.error.message;
210
+ break;
211
+ }
212
+ }
213
+ }
214
+
215
+ // If no stack found, generate one from current context
216
+ // Note: Angular's ErrorHandler doesn't pass Error objects, so this will
217
+ // capture the console.error call stack, not the original error stack.
218
+ // For better error tracking, use source maps or test with unminified builds.
219
+ if (!stack) {
220
+ const error = new Error();
221
+ stack = error.stack || 'No stack trace available';
222
+ message = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
223
+ }
224
+
225
+ this.enqueueEvent('console_error', {
226
+ message: message || 'Console error',
227
+ stack_trace: stack,
228
+ log_level: 'error',
229
+ metadata: {},
230
+ });
231
+ } catch (err) {
232
+ // Silently fail
233
+ }
234
+ };
235
+
236
+ // Global error handler
237
+ window.onerror = (message, source, lineno, colno, error) => {
238
+ try {
239
+ this.enqueueEvent('unhandled_error', {
240
+ message: String(message),
241
+ source: source || 'unknown',
242
+ lineno: lineno || 0,
243
+ colno: colno || 0,
244
+ stack_trace: error?.stack || 'No stack trace available',
245
+ log_level: 'error',
246
+ });
247
+ } catch (err) {
248
+ // Silently fail
249
+ }
250
+ return false;
251
+ };
252
+
253
+ // Unhandled promise rejection
254
+ window.addEventListener('unhandledrejection', (event) => {
255
+ this.enqueueEvent('unhandled_error', {
256
+ message: event.reason instanceof Error ? event.reason.message : String(event.reason),
257
+ stack_trace: event.reason?.stack || 'No stack trace available',
258
+ log_level: 'error',
259
+ });
260
+ });
261
+ }
262
+
263
+ /**
264
+ * Setup network monitoring
265
+ */
266
+ private setupNetworkMonitoring(): void {
267
+ // Store original fetch
268
+ if (!this.originalFetch) {
269
+ this.originalFetch = window.fetch;
270
+ }
271
+
272
+ // Override fetch
273
+ window.fetch = async (...args: any[]) => {
274
+ const url = args[0] instanceof Request ? args[0].url : args[0];
275
+ const method = args[0] instanceof Request && args[0].method
276
+ ? args[0].method
277
+ : args[1]?.method || 'GET';
278
+ const startTime = Date.now();
279
+
280
+ try {
281
+ const response = await this.originalFetch.apply(window, args);
282
+ const duration = Date.now() - startTime;
283
+ const status = response.status;
284
+
285
+ // Only track errors (4xx, 5xx)
286
+ if (status >= 400) {
287
+ const responseClone = response.clone();
288
+ try {
289
+ const text = await responseClone.text();
290
+ let responseData = null;
291
+ try {
292
+ const truncated = text.length > 5000 ? text.substring(0, 5000) + '...' : text;
293
+ responseData = JSON.parse(truncated);
294
+ } catch (e) {
295
+ responseData = text.length > 5000 ? text.substring(0, 5000) + '...' : text;
296
+ }
297
+
298
+ this.enqueueEvent('api_call', {
299
+ method,
300
+ url,
301
+ status_code: status,
302
+ response_data: responseData,
303
+ duration,
304
+ });
305
+ } catch (err) {
306
+ this.enqueueEvent('api_call', {
307
+ method,
308
+ url,
309
+ status_code: status,
310
+ duration,
311
+ error: 'Failed to read response body',
312
+ });
313
+ }
314
+ }
315
+
316
+ return response;
317
+ } catch (error: any) {
318
+ const duration = Date.now() - startTime;
319
+ this.enqueueEvent('api_call', {
320
+ method,
321
+ url,
322
+ status_code: 0,
323
+ response_data: error.message || 'Network Error',
324
+ duration,
325
+ error: error.message || 'Network Error',
326
+ });
327
+ throw error;
328
+ }
329
+ };
330
+
331
+ // Store original XMLHttpRequest
332
+ if (!this.originalXMLHttpRequest && (window as any).XMLHttpRequest) {
333
+ this.originalXMLHttpRequest = (window as any).XMLHttpRequest;
334
+ }
335
+
336
+ // Override XMLHttpRequest
337
+ const originalOpen = XMLHttpRequest.prototype.open;
338
+ const originalSend = XMLHttpRequest.prototype.send;
339
+
340
+ XMLHttpRequest.prototype.open = function (method: string, url: string) {
341
+ (this as any)._cuoralMethod = method;
342
+ (this as any)._cuoralUrl = url;
343
+ return originalOpen.apply(this, arguments as any);
344
+ };
345
+
346
+ XMLHttpRequest.prototype.send = function () {
347
+ const startTime = Date.now();
348
+ const xhr = this;
349
+
350
+ const loadendHandler = () => {
351
+ try {
352
+ const status = xhr.status;
353
+ const duration = Date.now() - startTime;
354
+
355
+ // Only track errors (4xx, 5xx)
356
+ if (status >= 400) {
357
+ let responseData = null;
358
+ try {
359
+ const responseText = xhr.responseText || '';
360
+ const truncated = responseText.length > 5000
361
+ ? responseText.substring(0, 5000) + '...'
362
+ : responseText;
363
+ responseData = truncated ? JSON.parse(truncated) : truncated;
364
+ } catch (e) {
365
+ const responseText = xhr.responseText || '';
366
+ responseData = responseText.length > 5000
367
+ ? responseText.substring(0, 5000) + '...'
368
+ : responseText;
369
+ }
370
+
371
+ (window as any)._cuoralIntelligence?.enqueueEvent('api_call', {
372
+ method: (xhr as any)._cuoralMethod,
373
+ url: (xhr as any)._cuoralUrl,
374
+ status_code: status,
375
+ response_data: responseData,
376
+ duration,
377
+ });
378
+ }
379
+ } catch (err) {
380
+ // Silently fail
381
+ }
382
+ };
383
+
384
+ this.addEventListener('loadend', loadendHandler, { once: true });
385
+ return originalSend.apply(this, arguments as any);
386
+ };
387
+
388
+ // Store reference for XMLHttpRequest monitoring
389
+ (window as any)._cuoralIntelligence = this;
390
+ }
391
+
392
+ /**
393
+ * Setup app state listener
394
+ */
395
+ private setupAppStateListener(): void {
396
+ // Listen for app going to background
397
+ window.addEventListener('visibilitychange', () => {
398
+ if (document.visibilityState === 'hidden') {
399
+ this.flush();
400
+ }
401
+ });
402
+
403
+ // Listen for app pause (Capacitor)
404
+ if (Capacitor.isNativePlatform()) {
405
+ window.addEventListener('pause', () => {
406
+ this.flush();
407
+ });
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Enqueue an event
413
+ */
414
+ private enqueueEvent(type: string, data: any): void {
415
+ const queue = this.queues[type];
416
+ if (!queue) {
417
+ return;
418
+ }
419
+
420
+ // Don't track successful API calls (2xx, 3xx)
421
+ if (type === 'api_call' && data.status_code >= 200 && data.status_code < 400) {
422
+ return;
423
+ }
424
+
425
+ // Limit queue size
426
+ if (queue.length >= this.config.maxQueueSize) {
427
+ queue.shift();
428
+ }
429
+
430
+ const event: QueueEvent = {
431
+ event_type: type,
432
+ timestamp: Date.now(),
433
+ session_id: this.sessionId || '',
434
+ user_agent: navigator.userAgent,
435
+ url: window.location.href,
436
+ data,
437
+ };
438
+
439
+ queue.push(event);
440
+
441
+ // Flush immediately for critical errors
442
+ const shouldFlushImmediately = type === 'api_call' && (data.status_code === 0 || data.status_code >= 400);
443
+
444
+ if (shouldFlushImmediately || queue.length >= this.config.batchSize) {
445
+ this.flushQueue(type);
446
+ } else if (!this.batchTimers[type]) {
447
+ this.batchTimers[type] = setTimeout(
448
+ () => this.flushQueue(type),
449
+ this.config.batchInterval
450
+ );
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Flush a specific queue
456
+ */
457
+ private flushQueue(eventType: string, useBeacon = false): void {
458
+ const queue = this.queues[eventType];
459
+ if (!queue || queue.length === 0) {
460
+ return;
461
+ }
462
+
463
+ // Clear timer
464
+ if (this.batchTimers[eventType]) {
465
+ clearTimeout(this.batchTimers[eventType]);
466
+ delete this.batchTimers[eventType];
467
+ }
468
+
469
+ const eventsToProcess = [...queue];
470
+ this.queues[eventType] = [];
471
+
472
+ let backendUrl: string;
473
+ let transformedPayload: any[];
474
+
475
+ if (eventType === 'console_error' || eventType === 'unhandled_error') {
476
+ backendUrl = this.config.consoleErrorBackendUrl;
477
+ transformedPayload = eventsToProcess.map((event) => ({
478
+ message: event.data.message,
479
+ stack_trace: event.data.stack_trace,
480
+ log_level: 'error',
481
+ url: event.url,
482
+ session_id: event.session_id,
483
+ source: 'mobile',
484
+ console_metadata: event.data.metadata || {},
485
+ }));
486
+ } else if (eventType === 'page_view') {
487
+ backendUrl = this.config.pageViewBackendUrl;
488
+ transformedPayload = eventsToProcess.map((event) => ({
489
+ session_id: event.session_id,
490
+ url: event.data.screen,
491
+ referrer: event.data.referrer,
492
+ user_agent: event.user_agent,
493
+ source: 'mobile',
494
+ page_metadata: {
495
+ duration: event.data.duration,
496
+ screen: event.data.screen,
497
+ ...event.data.metadata,
498
+ },
499
+ timestamp: new Date(event.timestamp).toISOString(),
500
+ }));
501
+ } else if (eventType === 'api_call') {
502
+ backendUrl = this.config.apiResponseBackendUrl;
503
+ transformedPayload = eventsToProcess
504
+ .map((event) => {
505
+ let responseBody = event.data.response_data;
506
+ if (typeof responseBody === 'string') {
507
+ try {
508
+ responseBody = JSON.parse(responseBody);
509
+ } catch (e) {
510
+ responseBody = { raw_text: responseBody };
511
+ }
512
+ } else if (responseBody === null || typeof responseBody === 'undefined') {
513
+ responseBody = {};
514
+ }
515
+
516
+ return {
517
+ url: event.data.url,
518
+ method: event.data.method,
519
+ status_code: event.data.status_code,
520
+ response_body: responseBody,
521
+ session_id: event.session_id,
522
+ source: 'mobile',
523
+ api_metadata: {
524
+ duration: event.data.duration,
525
+ error: event.data.error || null,
526
+ },
527
+ };
528
+ })
529
+ .filter((item) => item !== null && item !== undefined);
530
+ } else {
531
+ return;
532
+ }
533
+
534
+ const filteredPayload = transformedPayload.filter((item) => item !== null && item !== undefined);
535
+
536
+ if (backendUrl && filteredPayload.length > 0) {
537
+ this.sendToBackend(backendUrl, filteredPayload, useBeacon);
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Send data to backend
543
+ */
544
+ private async sendToBackend(url: string, payload: any[], useBeacon = false, retryCount = 0): Promise<void> {
545
+ if (!this.sessionId) {
546
+ this.pendingEvents.push({ url, payload, useBeacon });
547
+ return;
548
+ }
549
+
550
+ if (!navigator.onLine) {
551
+ return;
552
+ }
553
+
554
+ const data = JSON.stringify(payload);
555
+ const headers = {
556
+ 'Content-Type': 'application/json',
557
+ };
558
+
559
+ try {
560
+ const response = await fetch(url, {
561
+ method: 'POST',
562
+ headers,
563
+ body: data,
564
+ keepalive: useBeacon,
565
+ });
566
+
567
+ if (!response.ok && retryCount < this.config.retryAttempts) {
568
+ setTimeout(() => {
569
+ this.sendToBackend(url, payload, false, retryCount + 1);
570
+ }, this.config.retryDelay);
571
+ }
572
+ } catch (error) {
573
+ // Silently fail
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Flush pending events
579
+ */
580
+ private flushPendingEvents(): void {
581
+ if (this.pendingEvents.length > 0) {
582
+ const pending = [...this.pendingEvents];
583
+ this.pendingEvents = [];
584
+ pending.forEach(({ url, payload, useBeacon }) => {
585
+ this.sendToBackend(url, payload, useBeacon);
586
+ });
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Setup native error capture for Android/iOS crashes
592
+ */
593
+ private setupNativeErrorCapture(): void {
594
+ try {
595
+ // Only run on native platforms and if we have a session ID
596
+ if (Capacitor.isNativePlatform() && this.sessionId) {
597
+ CuoralPlugin.setupNativeErrorCapture({
598
+ backendUrl: this.config.consoleErrorBackendUrl,
599
+ sessionId: this.sessionId,
600
+ }).catch(() => {
601
+ // Silently fail - native error capture optional
602
+ });
603
+ }
604
+ } catch (error) {
605
+ // Silently fail if plugin is not available
606
+
607
+ }
608
+ }
609
+ }
package/src/plugin.ts CHANGED
@@ -43,11 +43,9 @@ export interface CuoralPluginInterface {
43
43
  }
44
44
 
45
45
  /**
46
- * Register the Capacitor plugin
46
+ * Register the Capacitor plugin (mobile only - iOS and Android)
47
47
  */
48
- const CuoralPlugin = registerPlugin<CuoralPluginInterface>('CuoralPlugin', {
49
- web: () => import('./web').then(m => new m.CuoralPluginWeb()),
50
- });
48
+ const CuoralPlugin = registerPlugin<CuoralPluginInterface>('CuoralPlugin');
51
49
 
52
50
  export { CuoralPlugin };
53
51
 
@@ -83,6 +81,7 @@ export class CuoralRecorder {
83
81
 
84
82
  // Start recording
85
83
  const result = await CuoralPlugin.startRecording(options);
84
+
86
85
  if (result.success) {
87
86
  this.isRecording = true;
88
87
  this.recordingStartTime = Date.now();
@@ -92,11 +91,23 @@ export class CuoralRecorder {
92
91
  type: CuoralMessageType.RECORDING_STARTED,
93
92
  payload: { timestamp: this.recordingStartTime },
94
93
  });
94
+ } else {
95
+ // Recording failed - reset state and notify widget
96
+ this.isRecording = false;
97
+ this.recordingStartTime = undefined;
98
+
99
+ this.postMessage({
100
+ type: CuoralMessageType.RECORDING_ERROR,
101
+ payload: { error: 'Failed to start recording' },
102
+ });
95
103
  }
96
104
 
97
105
  return result.success;
98
106
  } catch (error) {
99
- console.error('[Cuoral] Failed to start recording:', error);
107
+ // Exception occurred - reset state and notify widget
108
+ this.isRecording = false;
109
+ this.recordingStartTime = undefined;
110
+
100
111
  this.postMessage({
101
112
  type: CuoralMessageType.RECORDING_ERROR,
102
113
  payload: { error: (error as Error).message },
@@ -111,7 +122,11 @@ export class CuoralRecorder {
111
122
  async stopRecording(): Promise<{ filePath?: string; duration?: number } | null> {
112
123
  try {
113
124
  if (!this.isRecording) {
114
- console.warn('[Cuoral] Not recording');
125
+ // Send error message to widget so it can exit "stopping" state
126
+ this.postMessage({
127
+ type: CuoralMessageType.RECORDING_ERROR,
128
+ payload: { error: 'Not recording' },
129
+ });
115
130
  return null;
116
131
  }
117
132
 
@@ -137,6 +152,11 @@ export class CuoralRecorder {
137
152
  };
138
153
  }
139
154
 
155
+ // If result.success is false, send error to widget
156
+ this.postMessage({
157
+ type: CuoralMessageType.RECORDING_ERROR,
158
+ payload: { error: 'Failed to stop recording' },
159
+ });
140
160
  return null;
141
161
  } catch (error) {
142
162
  console.error('[Cuoral] Failed to stop recording:', error);
package/src/web.ts CHANGED
@@ -15,8 +15,6 @@ export class CuoralPluginWeb extends WebPlugin implements CuoralPluginInterface
15
15
  private recordingStartTime?: number;
16
16
 
17
17
  async startRecording(options?: RecordingOptions): Promise<{ success: boolean }> {
18
- console.log('[Cuoral Web] Start recording', options);
19
-
20
18
  // Check if Screen Capture API is available
21
19
  if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
22
20
  throw new Error('Screen recording is not supported in this browser');
@@ -43,8 +41,6 @@ export class CuoralPluginWeb extends WebPlugin implements CuoralPluginInterface
43
41
  }
44
42
 
45
43
  async stopRecording(): Promise<{ success: boolean; filePath?: string; duration?: number }> {
46
- console.log('[Cuoral Web] Stop recording');
47
-
48
44
  if (!this.isRecording) {
49
45
  return { success: false };
50
46
  }
@@ -79,8 +75,6 @@ export class CuoralPluginWeb extends WebPlugin implements CuoralPluginInterface
79
75
  }
80
76
 
81
77
  async takeScreenshot(options?: ScreenshotOptions): Promise<ScreenshotData> {
82
- console.log('[Cuoral Web] Take screenshot', options);
83
-
84
78
  try {
85
79
  // Use Screen Capture API
86
80
  const stream = await navigator.mediaDevices.getDisplayMedia({