@appxiom/web 0.1.8
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/LICENSE +5 -0
- package/README.md +313 -0
- package/ax_web_core/ax_web_core.d.ts +192 -0
- package/ax_web_core/ax_web_core.js +1541 -0
- package/ax_web_core/ax_web_core_bg.wasm +0 -0
- package/ax_web_core/ax_web_core_bg.wasm.d.ts +48 -0
- package/console/patch.js +10 -0
- package/core/ax.js +96 -0
- package/error/patch.js +47 -0
- package/event/ax-event.js +167 -0
- package/http/ax-http.js +63 -0
- package/index.d.ts +53 -0
- package/index.js +10 -0
- package/memory/ax-dom-leak.js +77 -0
- package/memory/ax-leak.js +107 -0
- package/package.json +60 -0
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_get_leakrecord_age: (a: number) => number;
|
|
5
|
+
export const __wbg_get_leakrecord_first_seen: (a: number) => bigint;
|
|
6
|
+
export const __wbg_get_leakrecord_gc_survivals: (a: number) => number;
|
|
7
|
+
export const __wbg_get_leakrecord_instance_id: (a: number) => number;
|
|
8
|
+
export const __wbg_get_leakrecord_last_gc_epoch: (a: number) => number;
|
|
9
|
+
export const __wbg_leakanalyzer_free: (a: number, b: number) => void;
|
|
10
|
+
export const __wbg_leakrecord_free: (a: number, b: number) => void;
|
|
11
|
+
export const __wbg_set_leakrecord_age: (a: number, b: number) => void;
|
|
12
|
+
export const __wbg_set_leakrecord_first_seen: (a: number, b: bigint) => void;
|
|
13
|
+
export const __wbg_set_leakrecord_gc_survivals: (a: number, b: number) => void;
|
|
14
|
+
export const __wbg_set_leakrecord_instance_id: (a: number, b: number) => void;
|
|
15
|
+
export const __wbg_set_leakrecord_last_gc_epoch: (a: number, b: number) => void;
|
|
16
|
+
export const end_goal: (a: number, b: number) => number;
|
|
17
|
+
export const get_device_state_js: () => number;
|
|
18
|
+
export const handle_api_after_request: (a: number, b: number, c: number, d: bigint) => number;
|
|
19
|
+
export const handle_api_after_response: (a: number, b: number, c: number, d: bigint, e: number, f: number, g: bigint, h: number) => number;
|
|
20
|
+
export const handle_api_on_error: (a: number, b: number, c: number, d: bigint, e: number) => number;
|
|
21
|
+
export const init: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number;
|
|
22
|
+
export const leakanalyzer_evaluate_leaks: (a: number) => number;
|
|
23
|
+
export const leakanalyzer_new: () => number;
|
|
24
|
+
export const leakanalyzer_notify_gc_epoch: (a: number) => number;
|
|
25
|
+
export const leakanalyzer_report_leak: (a: number) => number;
|
|
26
|
+
export const leakanalyzer_track_instance: (a: number, b: number) => number;
|
|
27
|
+
export const log_activity: (a: number, b: number, c: number) => number;
|
|
28
|
+
export const log_event: (a: number, b: number) => number;
|
|
29
|
+
export const log_event_activity: (a: number, b: number, c: number) => number;
|
|
30
|
+
export const log_resource_load_error: (a: number, b: number, c: number) => number;
|
|
31
|
+
export const report_custom_issue: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number;
|
|
32
|
+
export const report_error: (a: number) => number;
|
|
33
|
+
export const set_custom_id: (a: number, b: number) => number;
|
|
34
|
+
export const start_goal: (a: number, b: number) => number;
|
|
35
|
+
export const __wasm_bindgen_func_elem_458: (a: number, b: number, c: number) => void;
|
|
36
|
+
export const __wasm_bindgen_func_elem_1045: (a: number, b: number, c: number, d: number) => void;
|
|
37
|
+
export const __wasm_bindgen_func_elem_1102: (a: number, b: number, c: number, d: number) => void;
|
|
38
|
+
export const __wasm_bindgen_func_elem_455: (a: number, b: number, c: number) => void;
|
|
39
|
+
export const __wasm_bindgen_func_elem_461: (a: number, b: number, c: number) => void;
|
|
40
|
+
export const __wasm_bindgen_func_elem_461_4: (a: number, b: number, c: number) => void;
|
|
41
|
+
export const __wasm_bindgen_func_elem_450: (a: number, b: number, c: number) => void;
|
|
42
|
+
export const __wasm_bindgen_func_elem_465: (a: number, b: number) => void;
|
|
43
|
+
export const __wbindgen_export: (a: number, b: number) => number;
|
|
44
|
+
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
45
|
+
export const __wbindgen_export3: (a: number) => void;
|
|
46
|
+
export const __wbindgen_export4: (a: number, b: number) => void;
|
|
47
|
+
export const __wbindgen_export5: (a: number, b: number, c: number) => void;
|
|
48
|
+
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
package/console/patch.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import AX from '../core/ax.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Add a new function ax to console object. It should have blue color and aprefix with "AX: "
|
|
6
|
+
const log_api = AX.instance.wasm.log_api;
|
|
7
|
+
console.ax = (message) => {
|
|
8
|
+
//log_api(`AX: ${message}`);
|
|
9
|
+
console.log(`%cAX: ${message}`, 'color: blue; font-weight: bold;');
|
|
10
|
+
};
|
package/core/ax.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton class representing the AX-JS core module.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as wasm from '../ax_web_core/ax_web_core.js';
|
|
6
|
+
|
|
7
|
+
export default class AX {
|
|
8
|
+
// Static instance
|
|
9
|
+
static instance = null;
|
|
10
|
+
|
|
11
|
+
// Application and platform keys
|
|
12
|
+
appKey;
|
|
13
|
+
platformKey;
|
|
14
|
+
version;
|
|
15
|
+
buildIdentifier;
|
|
16
|
+
wasm;
|
|
17
|
+
|
|
18
|
+
// Singleton pattern
|
|
19
|
+
|
|
20
|
+
constructor(appKey, platformKey, version, buildIdentifier) {
|
|
21
|
+
if (AX.instance) return AX.instance;
|
|
22
|
+
this.wasm = wasm;
|
|
23
|
+
this.appKey = appKey;
|
|
24
|
+
this.platformKey = platformKey;
|
|
25
|
+
this.version = version;
|
|
26
|
+
this.buildIdentifier = buildIdentifier;
|
|
27
|
+
AX.instance = this;
|
|
28
|
+
// Additional initialization logic can go here
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Launch the AX module
|
|
32
|
+
async launch() {
|
|
33
|
+
// Load wasm module
|
|
34
|
+
await this.wasm.default();
|
|
35
|
+
// Load runtime patches synchronously using dynamic imports
|
|
36
|
+
await import('../console/patch.js');
|
|
37
|
+
await import('../error/patch.js');
|
|
38
|
+
await import('../http/ax-http.js');
|
|
39
|
+
await import('../event/ax-event.js');
|
|
40
|
+
await import('../memory/ax-dom-leak.js');
|
|
41
|
+
|
|
42
|
+
// initialize the wasm module with app key, platform key, version, and build identifier
|
|
43
|
+
await this.wasm.init(this.appKey, this.platformKey, this.version, this.buildIdentifier);
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Start a goal with the given name
|
|
48
|
+
startGoal(name) {
|
|
49
|
+
if (this.wasm && this.wasm.start_goal) {
|
|
50
|
+
this.wasm.start_goal(name);
|
|
51
|
+
} else {
|
|
52
|
+
// console.warn("AX Wasm not loaded, cannot start goal:", name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// End a goal with the given name, properties, and value
|
|
57
|
+
endGoal(name, properties = {}, value = null) {
|
|
58
|
+
if (this.wasm && this.wasm.end_goal) {
|
|
59
|
+
this.wasm.end_goal(name, "Success", properties, value);
|
|
60
|
+
} else {
|
|
61
|
+
// console.warn("AX Wasm not loaded, cannot end goal:", name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Report a custom issue
|
|
66
|
+
// Severity can be
|
|
67
|
+
// - 0 -> Minor
|
|
68
|
+
// - 1 -> Major
|
|
69
|
+
// - 2 -> Fatal
|
|
70
|
+
reportCustomIssue(title, description, severity, data = {}, stacktrace = null) {
|
|
71
|
+
if (this.wasm && this.wasm.report_custom_issue) {
|
|
72
|
+
this.wasm.report_custom_issue(title, description, severity, data, stacktrace);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Set a custom ID
|
|
77
|
+
setCustomId(customId) {
|
|
78
|
+
if (this.wasm && this.wasm.set_custom_id) {
|
|
79
|
+
this.wasm.set_custom_id(customId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Log activity with the given name and properties
|
|
84
|
+
logActivity(name, properties = {}) {
|
|
85
|
+
if (this.wasm && this.wasm.log_activity) {
|
|
86
|
+
this.wasm.log_activity(name, properties);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Log an event activity with the given name and properties
|
|
91
|
+
logEventActivity(name, properties = {}) {
|
|
92
|
+
if (this.wasm && this.wasm.log_event_activity) {
|
|
93
|
+
this.wasm.log_event_activity(name, properties);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/error/patch.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import AX from '../core/ax.js';
|
|
2
|
+
|
|
3
|
+
// 1. Patch Sync errors.
|
|
4
|
+
// Store old onerror handler if it exists
|
|
5
|
+
const oldOnError = window.onerror;
|
|
6
|
+
window.onerror = function(message, source, lineno, colno, error) {
|
|
7
|
+
if (oldOnError) {
|
|
8
|
+
try {
|
|
9
|
+
oldOnError(message, source, lineno, colno, error);
|
|
10
|
+
} catch (e) {}
|
|
11
|
+
}
|
|
12
|
+
handleOnError(1, message, source, lineno, colno, error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function handleOnError(category, message, source, lineno, colno, error) {
|
|
16
|
+
let e = {
|
|
17
|
+
category: category,
|
|
18
|
+
message: message,
|
|
19
|
+
fileName: source,
|
|
20
|
+
lineNumber: lineno,
|
|
21
|
+
columnNumber: colno,
|
|
22
|
+
stacktrace: error ? error.stack : null
|
|
23
|
+
};
|
|
24
|
+
AX.instance.wasm.report_error(e);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Async errors
|
|
28
|
+
window.addEventListener('unhandledrejection', function(event) {
|
|
29
|
+
handleOnError(2, event.reason ? event.reason.message : null, null, null, null, event.reason);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 3. Late handled promises - rejectionhandled
|
|
33
|
+
window.addEventListener('rejectionhandled', function(event) {
|
|
34
|
+
handleOnError(3, event.reason ? event.reason.message : null, null, null, null, event.reason);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 4. Resource loading errors
|
|
38
|
+
window.addEventListener('error', function(event) {
|
|
39
|
+
if (event.target && (event.target.src || event.target.href)) {
|
|
40
|
+
const url = event.target.src || event.target.href;
|
|
41
|
+
AX.instance.wasm.log_resource_load_error(url, {
|
|
42
|
+
message: event.message || 'Resource load error',
|
|
43
|
+
tagName: event.target.tagName,
|
|
44
|
+
innerHTML: event.target.innerHTML,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}, true);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import AX from '../core/ax.js';
|
|
2
|
+
|
|
3
|
+
// Track page load and unload events
|
|
4
|
+
function reportExit() {
|
|
5
|
+
// navigator.sendBeacon(
|
|
6
|
+
// "/telemetry",
|
|
7
|
+
// JSON.stringify({
|
|
8
|
+
// type: "page_exit",
|
|
9
|
+
// url: location.href,
|
|
10
|
+
// ts: Date.now(),
|
|
11
|
+
// })
|
|
12
|
+
// );
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Track page load
|
|
16
|
+
window.addEventListener("load", () => {
|
|
17
|
+
AX.instance.wasm.log_event_activity("Page Load", {
|
|
18
|
+
url: location.href,
|
|
19
|
+
referrer: document.referrer,
|
|
20
|
+
userAgent: navigator.userAgent,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// // Track page unload
|
|
25
|
+
// document.addEventListener("visibilitychange", () => {
|
|
26
|
+
// if (document.visibilityState === "hidden") {
|
|
27
|
+
// AX.instance.wasm.log_event_activity("Page out of visibility", {
|
|
28
|
+
// url: location.href,
|
|
29
|
+
// ts: Date.now(),
|
|
30
|
+
// });
|
|
31
|
+
// reportExit();
|
|
32
|
+
// } else if (document.visibilityState === "visible") {
|
|
33
|
+
// AX.instance.wasm.log_event_activity("Page in visibility", {
|
|
34
|
+
// url: location.href,
|
|
35
|
+
// ts: Date.now(),
|
|
36
|
+
// });
|
|
37
|
+
// }
|
|
38
|
+
// });
|
|
39
|
+
|
|
40
|
+
// // Also track pagehide for older browsers
|
|
41
|
+
// window.addEventListener("pagehide", () => {
|
|
42
|
+
// AX.instance.wasm.log_event_activity("Page Unload", {
|
|
43
|
+
// url: location.href,
|
|
44
|
+
// ts: Date.now(),
|
|
45
|
+
// });
|
|
46
|
+
// reportExit();
|
|
47
|
+
// });
|
|
48
|
+
|
|
49
|
+
// Track SPA route changes (if using a SPA framework)
|
|
50
|
+
// This is a generic example; you may need to adapt it based on your SPA framework.
|
|
51
|
+
window.addEventListener("popstate", () => {
|
|
52
|
+
AX.instance.wasm.log_event_activity("SPA Route Change", {
|
|
53
|
+
url: location.href,
|
|
54
|
+
ts: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Track hash changes for SPA routing
|
|
59
|
+
window.addEventListener("hashchange", () => {
|
|
60
|
+
AX.instance.wasm.log_event_activity("Hash Change", {
|
|
61
|
+
url: location.href,
|
|
62
|
+
ts: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Patch pushState and replaceState to track SPA route changes
|
|
67
|
+
(function () {
|
|
68
|
+
const originalPushState = history.pushState;
|
|
69
|
+
const originalReplaceState = history.replaceState;
|
|
70
|
+
|
|
71
|
+
history.pushState = function (...args) {
|
|
72
|
+
AX.instance.wasm.log_event_activity("SPA Route Change (pushState)", {
|
|
73
|
+
url: location.href,
|
|
74
|
+
ts: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
// dispatch event ax-push-state.
|
|
77
|
+
const pushStateEvent = new Event('ax-push-state');
|
|
78
|
+
window.dispatchEvent(pushStateEvent);
|
|
79
|
+
|
|
80
|
+
const result = originalPushState.apply(this, args);
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
history.replaceState = function (...args) {
|
|
85
|
+
AX.instance.wasm.log_event_activity("SPA Route Change (replaceState)", {
|
|
86
|
+
url: location.href,
|
|
87
|
+
ts: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
const replaceStateEvent = new Event('ax-replace-state');
|
|
90
|
+
window.dispatchEvent(replaceStateEvent);
|
|
91
|
+
|
|
92
|
+
const result = originalReplaceState.apply(this, args);
|
|
93
|
+
return result;
|
|
94
|
+
};
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
// Track clicks on buttons and links
|
|
98
|
+
document.addEventListener('click', (event) => {
|
|
99
|
+
const target = event.target.closest('button, a, li');
|
|
100
|
+
if (!target) return;
|
|
101
|
+
|
|
102
|
+
const info = {
|
|
103
|
+
type: event.type,
|
|
104
|
+
tag: target.tagName.toLowerCase(),
|
|
105
|
+
text: target.innerText.trim(),
|
|
106
|
+
href: target.getAttribute('href'),
|
|
107
|
+
x: event.clientX,
|
|
108
|
+
y: event.clientY,
|
|
109
|
+
ctrl: event.ctrlKey,
|
|
110
|
+
shift: event.shiftKey,
|
|
111
|
+
meta: event.metaKey,
|
|
112
|
+
button: event.button,
|
|
113
|
+
timestamp: event.timeStamp,
|
|
114
|
+
url: window.location.href,
|
|
115
|
+
path: event.composedPath().map(el => el.tagName || el.nodeName),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
AX.instance.wasm.log_event_activity(`Clicked - ${info.tag} - ${info.text}`, info);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Track form elements
|
|
122
|
+
// listen for events after focus is changed from the element.
|
|
123
|
+
document.addEventListener('change', function (event) {
|
|
124
|
+
const el = event.target;
|
|
125
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
|
|
126
|
+
return; // Ignore non-form elements
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fieldType = el.type || el.tagName.toLowerCase();
|
|
130
|
+
let valueSummary;
|
|
131
|
+
|
|
132
|
+
switch (fieldType) {
|
|
133
|
+
case 'checkbox':
|
|
134
|
+
case 'radio':
|
|
135
|
+
valueSummary = el.checked;
|
|
136
|
+
break;
|
|
137
|
+
case 'select-one':
|
|
138
|
+
valueSummary = el.selectedIndex; // or el.value if it's non-sensitive
|
|
139
|
+
break;
|
|
140
|
+
case 'select-multiple':
|
|
141
|
+
valueSummary = Array.from(el.selectedOptions).map(o => o.index);
|
|
142
|
+
break;
|
|
143
|
+
case 'range':
|
|
144
|
+
case 'number':
|
|
145
|
+
valueSummary = Number(el.value);
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
valueSummary = el.value.length; // just length, not content
|
|
149
|
+
name = el.name || null;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = {
|
|
154
|
+
tag: el.tagName.toLowerCase(),
|
|
155
|
+
type: fieldType,
|
|
156
|
+
name: el.name || null,
|
|
157
|
+
formName: el.form ? el.form.name : null,
|
|
158
|
+
formId: el.form ? el.form.id : null,
|
|
159
|
+
length: typeof valueSummary === 'number' ? valueSummary : 0,
|
|
160
|
+
value: valueSummary,//typeof valueSummary !== 'number' ? valueSummary : "*",
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Example: send to your analytics endpoint
|
|
165
|
+
// navigator.sendBeacon('/track', JSON.stringify({ event: 'form_input', ...data }));
|
|
166
|
+
AX.instance.wasm.log_event_activity(`FormFill - ${data.tag} - ${data.name}`, data);
|
|
167
|
+
});
|
package/http/ax-http.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import AX from '../core/ax.js';
|
|
2
|
+
|
|
3
|
+
// Track fetch
|
|
4
|
+
const originalFetch = window.fetch;
|
|
5
|
+
window.fetch = async function(...args) {
|
|
6
|
+
const [resource, init] = args;
|
|
7
|
+
const request = resource instanceof Request && init == null
|
|
8
|
+
? resource.clone()
|
|
9
|
+
: new Request(resource, init);
|
|
10
|
+
|
|
11
|
+
// If request headers contain X-App-Key, skip tracking to avoid infinite loop
|
|
12
|
+
if (request.headers.get('X-App-Key'))
|
|
13
|
+
return originalFetch.apply(this, args);
|
|
14
|
+
|
|
15
|
+
const url = request.url;
|
|
16
|
+
const requestTime = BigInt(Date.now());
|
|
17
|
+
|
|
18
|
+
handleApiCallAfterRequest(url, request, requestTime);
|
|
19
|
+
|
|
20
|
+
// Clone response to safely read body
|
|
21
|
+
let response = null;
|
|
22
|
+
try {
|
|
23
|
+
response = await originalFetch.apply(this, args);
|
|
24
|
+
let clonedResponse = response.clone();
|
|
25
|
+
handleApiCallAfterResponse(url, request, requestTime, clonedResponse, BigInt(Date.now()), new Error("Request Location:"));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
handleApiCallOnError(url, request, requestTime, e);
|
|
28
|
+
throw e; // Re-throw the error to maintain the original behavior
|
|
29
|
+
}
|
|
30
|
+
return response;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Track XMLHttpRequest
|
|
34
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
35
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
36
|
+
this._apiTrackerUrl = url;
|
|
37
|
+
this._apiTrackerMethod = method;
|
|
38
|
+
let result = originalOpen.apply(this, [method, url, ...rest]);
|
|
39
|
+
// console.ax(`[API TRACKER] XMLHttpRequest opened: ${method} ${url}`);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
43
|
+
XMLHttpRequest.prototype.send = function(...args) {
|
|
44
|
+
// if (console.ax && this._apiTrackerUrl && (this._apiTrackerUrl.startsWith('http://') || this._apiTrackerUrl.startsWith('https://')))
|
|
45
|
+
// console.ax(`[API TRACKER] XHR request: ${this._apiTrackerMethod} ${this._apiTrackerUrl} ${JSON.stringify(args[0] || {})}`);
|
|
46
|
+
this.addEventListener('loadend', function() {
|
|
47
|
+
// if (console.ax && this._apiTrackerUrl && (this._apiTrackerUrl.startsWith('http://') || this._apiTrackerUrl.startsWith('https://')))
|
|
48
|
+
// console.ax(`[API TRACKER] XHR response: ${this._apiTrackerMethod} ${this._apiTrackerUrl} status=${this.status} response=${this.response}`);
|
|
49
|
+
});
|
|
50
|
+
return originalSend.apply(this, args);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
async function handleApiCallAfterResponse(url, request, requestTime, response, responseTime, error) {
|
|
54
|
+
AX.instance.wasm.handle_api_after_response(url, request, requestTime, response, responseTime, error);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handleApiCallAfterRequest(url, request, requestTime) {
|
|
58
|
+
AX.instance.wasm.handle_api_after_request(url, request, requestTime);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleApiCallOnError(url, request, requestTime, error) {
|
|
62
|
+
AX.instance.wasm.handle_api_on_error(url, request, requestTime, error);
|
|
63
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type IssueSeverity = 0 | 1 | 2;
|
|
2
|
+
|
|
3
|
+
export interface AppxiomProperties {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class AX {
|
|
8
|
+
static instance: AX | null;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
appKey: string,
|
|
12
|
+
platformKey: string,
|
|
13
|
+
version: string,
|
|
14
|
+
buildIdentifier?: string,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
launch(): Promise<void>;
|
|
18
|
+
startGoal(name: string): void;
|
|
19
|
+
endGoal<TProperties extends object = AppxiomProperties>(
|
|
20
|
+
name: string,
|
|
21
|
+
properties?: TProperties,
|
|
22
|
+
value?: number | null,
|
|
23
|
+
): void;
|
|
24
|
+
reportCustomIssue<TData extends object = AppxiomProperties>(
|
|
25
|
+
title: string,
|
|
26
|
+
description: string,
|
|
27
|
+
severity: IssueSeverity,
|
|
28
|
+
data?: TData,
|
|
29
|
+
stacktrace?: string | null,
|
|
30
|
+
): void;
|
|
31
|
+
setCustomId(customId: string): void;
|
|
32
|
+
logActivity<TProperties extends object = AppxiomProperties>(
|
|
33
|
+
name: string,
|
|
34
|
+
properties?: TProperties,
|
|
35
|
+
): void;
|
|
36
|
+
logEventActivity<TProperties extends object = AppxiomProperties>(
|
|
37
|
+
name: string,
|
|
38
|
+
properties?: TProperties,
|
|
39
|
+
): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class LeakTracker {
|
|
43
|
+
constructor(type?: number);
|
|
44
|
+
|
|
45
|
+
trackInstance(instance: object, snapshot?: unknown): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function init(
|
|
49
|
+
appKey: string,
|
|
50
|
+
platformKey: string,
|
|
51
|
+
version: string,
|
|
52
|
+
buildIdentifier?: string,
|
|
53
|
+
): Promise<AX>;
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import AX from './core/ax.js';
|
|
2
|
+
export { LeakTracker } from './memory/ax-leak.js';
|
|
3
|
+
|
|
4
|
+
export default async function init(appKey, platformKey, version, buildIdentifier) {
|
|
5
|
+
const ax = new AX(appKey, platformKey, version, buildIdentifier);
|
|
6
|
+
await ax.launch();
|
|
7
|
+
return ax;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { AX };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { LeakTracker } from './ax-leak.js';
|
|
2
|
+
|
|
3
|
+
/** Dom Leak Tracker */
|
|
4
|
+
class DomLeakTracker extends LeakTracker {
|
|
5
|
+
// Constructor
|
|
6
|
+
constructor() {
|
|
7
|
+
super(1); // DOM Leak Type
|
|
8
|
+
|
|
9
|
+
// Mutation Observer to track removed nodes
|
|
10
|
+
new MutationObserver(mutations => {
|
|
11
|
+
for (const m of mutations) {
|
|
12
|
+
for (const node of m.removedNodes) {
|
|
13
|
+
if (node.nodeType === Node.ELEMENT_NODE
|
|
14
|
+
&& node.tagName.toUpperCase() !== 'SCRIPT'
|
|
15
|
+
&& node.tagName.toUpperCase() !== 'STYLE'
|
|
16
|
+
&& node.tagName.toUpperCase() !== 'BR'
|
|
17
|
+
&& node.tagName.toUpperCase() !== 'HR'
|
|
18
|
+
&& node.tagName.toUpperCase() !== 'WBR'
|
|
19
|
+
) {
|
|
20
|
+
this.trackInstance(node, this.serializeWithParents(node));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}).observe(document.documentElement, {
|
|
25
|
+
childList: true,
|
|
26
|
+
subtree: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for Leak - Overriding here.
|
|
31
|
+
// A DOM element is considered leaking if it is not connected to the document.
|
|
32
|
+
isLeaking (instanceRecord) {
|
|
33
|
+
if (!super.isLeaking(instanceRecord)) return false;
|
|
34
|
+
const instance = instanceRecord.instanceRef.deref();
|
|
35
|
+
return !instance.isConnected;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Serialize Element
|
|
39
|
+
serializeElement = (el) => {
|
|
40
|
+
if (!el || el.nodeType !== Node.ELEMENT_NODE) return null;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
tag: el.tagName.toLowerCase(),
|
|
44
|
+
id: el.id || null,
|
|
45
|
+
classes: el.className || null,
|
|
46
|
+
attributes: [...el.attributes].reduce((acc, attr) => {
|
|
47
|
+
acc[attr.name] = attr.value;
|
|
48
|
+
return acc;
|
|
49
|
+
}, {}),
|
|
50
|
+
isConnected: el.isConnected
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Serialize with Parents
|
|
55
|
+
serializeWithParents = (node, maxDepth = 5) => {
|
|
56
|
+
const chain = [];
|
|
57
|
+
let current = node;
|
|
58
|
+
let depth = 0;
|
|
59
|
+
|
|
60
|
+
while (current && depth < maxDepth) {
|
|
61
|
+
const serialized = this.serializeElement(current);
|
|
62
|
+
if (!serialized) break;
|
|
63
|
+
|
|
64
|
+
chain.push(serialized);
|
|
65
|
+
current = current.parentElement;
|
|
66
|
+
depth++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
depth: chain.length,
|
|
71
|
+
chain
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Initialize DomLeakTracker
|
|
77
|
+
new DomLeakTracker();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import AX from '../core/ax.js';
|
|
2
|
+
|
|
3
|
+
// Instance Record
|
|
4
|
+
class InstanceRecord {
|
|
5
|
+
constructor(id, instance, snapshot) {
|
|
6
|
+
this.id = id;
|
|
7
|
+
this.instanceRef = new WeakRef(instance); // WeakRef
|
|
8
|
+
this.snapshot = snapshot; // Serialized snapshot
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Base Leak Tracker */
|
|
13
|
+
export class LeakTracker {
|
|
14
|
+
|
|
15
|
+
// Leak Type
|
|
16
|
+
static LeakType = 0;
|
|
17
|
+
// LeakAnalyzer instance
|
|
18
|
+
leakAnalyzer = null;
|
|
19
|
+
|
|
20
|
+
// Weak maps for tracking instances
|
|
21
|
+
instanceIds = null;
|
|
22
|
+
instanceRefs = null;
|
|
23
|
+
nextInstanceId = 0;
|
|
24
|
+
type = 0;
|
|
25
|
+
|
|
26
|
+
// FinalizationRegistry for GC epoch signaling
|
|
27
|
+
gcRegistry = new FinalizationRegistry(async () => {
|
|
28
|
+
this.leakAnalyzer.notify_gc_epoch();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Constructor
|
|
32
|
+
constructor(type = 0) {
|
|
33
|
+
if (typeof WeakRef == "undefined" || typeof FinalizationRegistry == "undefined") return;
|
|
34
|
+
this.type = type;
|
|
35
|
+
this.leakAnalyzer = new AX.instance.wasm.LeakAnalyzer();
|
|
36
|
+
this.instanceIds = new WeakMap();
|
|
37
|
+
this.instanceRefs = new Map();
|
|
38
|
+
this.nextInstanceId++;
|
|
39
|
+
|
|
40
|
+
// FinalizationRegistry for GC epoch signaling
|
|
41
|
+
this.gcRegistry = new FinalizationRegistry(async () => {
|
|
42
|
+
this.leakAnalyzer.notify_gc_epoch();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Detection Loop
|
|
46
|
+
setInterval(() => {
|
|
47
|
+
// Schedule a GC observation signal
|
|
48
|
+
this.scheduleGcSignal();
|
|
49
|
+
this.detectLeaks();
|
|
50
|
+
|
|
51
|
+
}, 30_000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Schedule GC Signal
|
|
55
|
+
scheduleGcSignal = async () => {
|
|
56
|
+
const sentinel = {};
|
|
57
|
+
this.gcRegistry.register(sentinel, null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Track Instance
|
|
61
|
+
trackInstance = (instance, snapshot) => {
|
|
62
|
+
if (!instance) return;
|
|
63
|
+
let id = this.instanceIds.get(instance);
|
|
64
|
+
if (!id) {
|
|
65
|
+
id = this.nextInstanceId++;
|
|
66
|
+
this.instanceIds.set(instance, id);
|
|
67
|
+
this.instanceRefs.set(id, new InstanceRecord(id, instance, snapshot));
|
|
68
|
+
}
|
|
69
|
+
this.leakAnalyzer.track_instance(id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve Instance by ID
|
|
73
|
+
resolveInstanceById = (id) => {
|
|
74
|
+
const instanceRecord = this.instanceRefs.get(id);
|
|
75
|
+
if (!instanceRecord) return null;
|
|
76
|
+
const instance = instanceRecord.instanceRef.deref();
|
|
77
|
+
if (!instance) {
|
|
78
|
+
// GC collected → cleanup
|
|
79
|
+
this.instanceRefs.delete(id);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return instanceRecord;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for Leak - Override in subclass
|
|
86
|
+
isLeaking(instanceRecord) {
|
|
87
|
+
// Override in subclass
|
|
88
|
+
if (!instanceRecord || !instanceRecord.instanceRef.deref()) return false;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Detect Leaks
|
|
93
|
+
detectLeaks = async () => {
|
|
94
|
+
let leaks = await this.leakAnalyzer.evaluate_leaks();
|
|
95
|
+
for (const rec of leaks) {
|
|
96
|
+
const instanceRecord = this.resolveInstanceById(rec.instance_id);
|
|
97
|
+
if (this.isLeaking(instanceRecord)) {
|
|
98
|
+
this.reportLeak(instanceRecord);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Report Leak
|
|
104
|
+
reportLeak = (instanceRecord) => {
|
|
105
|
+
// Override in subclass
|
|
106
|
+
}
|
|
107
|
+
}
|