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.
- package/README.md +81 -4
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build/.transforms/2c48e1f34ca03014b78fcb3e0ab7197b/results.bin +1 -0
- package/android/build/.transforms/2c48e1f34ca03014b78fcb3e0ab7197b/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/.transforms/980c51bd075f726f311ad662d5d20ba0/results.bin +1 -0
- package/android/build/.transforms/980c51bd075f726f311ad662d5d20ba0/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/results.bin +1 -0
- package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/results.bin +1 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/ScreenRecordService.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/AndroidManifest.xml +22 -0
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/output-metadata.json +18 -0
- package/android/build/intermediates/aar_main_jar/debug/classes.jar +0 -0
- package/android/build/intermediates/aar_metadata/debug/aar-metadata.properties +6 -0
- package/android/build/intermediates/annotation_processor_list/debug/annotationProcessors.json +1 -0
- package/android/build/intermediates/annotations_typedef_file/debug/typedefs.txt +0 -0
- package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
- package/android/build/intermediates/compile_r_class_jar/debug/R.jar +0 -0
- package/android/build/intermediates/compile_symbol_list/debug/R.txt +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
- package/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state +0 -0
- package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
- package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +2 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/ScreenRecordService.class +0 -0
- package/android/build/intermediates/local_only_symbol_list/debug/R-def.txt +2 -0
- package/android/build/intermediates/manifest_merge_blame_file/debug/manifest-merger-blame-debug-report.txt +38 -0
- package/android/build/intermediates/merged_java_res/debug/feature-cuoral-ionic.jar +0 -0
- package/android/build/intermediates/merged_manifest/debug/AndroidManifest.xml +22 -0
- package/android/build/intermediates/navigation_json/debug/navigation.json +1 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/ScreenRecordService.class +0 -0
- package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
- package/android/build/intermediates/symbol_list_with_package_name/debug/package-aware-r.txt +1 -0
- package/android/build/outputs/aar/cuoral-ionic-debug.aar +0 -0
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +49 -0
- package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin$1.class.uniqueId1 +0 -0
- package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin$2.class.uniqueId2 +0 -0
- package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin.class.uniqueId0 +0 -0
- package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +290 -33
- package/android/src/main/java/com/cuoral/ionic/ScreenRecordService.java +59 -0
- package/dist/cuoral.d.ts +30 -1
- package/dist/cuoral.d.ts.map +1 -1
- package/dist/cuoral.js +168 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +706 -127
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +706 -126
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +66 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +508 -0
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +24 -6
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +0 -3
- package/ios/Plugin/CuoralPlugin.swift +78 -1
- package/package.json +1 -1
- package/src/cuoral.ts +205 -1
- package/src/index.ts +1 -0
- package/src/intelligence.ts +609 -0
- package/src/plugin.ts +26 -6
- 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
|
-
|
|
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
|
-
|
|
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({
|