@sentienguard/apm 1.0.4 → 1.0.5
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/package.json +1 -1
- package/src/browser/aggregator.js +38 -50
- package/src/browser/instrumentation.js +16 -19
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser Metrics Aggregator
|
|
3
3
|
*
|
|
4
|
-
* Batches browser
|
|
4
|
+
* Batches browser metrics in memory and flushes them periodically.
|
|
5
5
|
* Produces payloads compatible with the backend's ingestApmData controller.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - Outgoing fetch/XHR requests (
|
|
9
|
-
* - Page load timing (Navigation Timing API)
|
|
10
|
-
* - Web Vitals (LCP, FID, INP, CLS)
|
|
7
|
+
* Tracks:
|
|
8
|
+
* - Outgoing fetch/XHR as requests (keyed by method:route)
|
|
11
9
|
* - Unhandled JS errors
|
|
10
|
+
* - Web Vitals (LCP, FID, INP, CLS)
|
|
11
|
+
* - Page load timing
|
|
12
12
|
* - SPA route changes
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import config from './config.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Create an aggregation key for
|
|
18
|
+
* Create an aggregation key for request metrics
|
|
19
19
|
*/
|
|
20
|
-
function
|
|
21
|
-
return `${
|
|
20
|
+
function createRequestKey(method, route) {
|
|
21
|
+
return `${method}:${route}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export class BrowserMetricsAggregator {
|
|
@@ -28,12 +28,9 @@ export class BrowserMetricsAggregator {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
reset() {
|
|
31
|
-
// Outgoing HTTP requests (fetch/XHR) aggregated by
|
|
32
|
-
// Same shape as Node SDK
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
// Page load timing entries from Navigation Timing API
|
|
36
|
-
this.pageLoads = [];
|
|
31
|
+
// Outgoing HTTP requests (fetch/XHR) aggregated by method:route
|
|
32
|
+
// Same shape as Node.js SDK requests: { method, route, count, errorCount, latency }
|
|
33
|
+
this.requests = new Map();
|
|
37
34
|
|
|
38
35
|
// Web Vitals - latest values per metric name
|
|
39
36
|
this.webVitals = {};
|
|
@@ -43,32 +40,33 @@ export class BrowserMetricsAggregator {
|
|
|
43
40
|
|
|
44
41
|
// SPA route change events
|
|
45
42
|
this.routeChanges = [];
|
|
43
|
+
|
|
44
|
+
// Page load timing entries
|
|
45
|
+
this.pageLoads = [];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Record an outgoing HTTP request (fetch/XHR)
|
|
50
|
-
* Uses the same dependency shape as the Node.js SDK so the backend
|
|
51
|
-
* can process it identically.
|
|
52
50
|
*
|
|
53
|
-
* @param {string}
|
|
54
|
-
* @param {string}
|
|
51
|
+
* @param {string} method - HTTP method (GET, POST, PATCH, DELETE)
|
|
52
|
+
* @param {string} route - URL pathname (e.g., /api/todos)
|
|
55
53
|
* @param {number} latency - Response time in ms
|
|
56
|
-
* @param {boolean} isError - Whether the request failed
|
|
54
|
+
* @param {boolean} isError - Whether the request failed (4xx/5xx or network error)
|
|
57
55
|
*/
|
|
58
|
-
|
|
59
|
-
const key =
|
|
56
|
+
recordRequest(method, route, latency, isError = false) {
|
|
57
|
+
const key = createRequestKey(method, route);
|
|
60
58
|
|
|
61
|
-
let metric = this.
|
|
59
|
+
let metric = this.requests.get(key);
|
|
62
60
|
if (!metric) {
|
|
63
|
-
if (this.
|
|
61
|
+
if (this.requests.size >= this.maxRoutes) return;
|
|
64
62
|
metric = {
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
method,
|
|
64
|
+
route,
|
|
67
65
|
count: 0,
|
|
68
66
|
errorCount: 0,
|
|
69
67
|
latency: { sum: 0, min: Infinity, max: 0 }
|
|
70
68
|
};
|
|
71
|
-
this.
|
|
69
|
+
this.requests.set(key, metric);
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
metric.count++;
|
|
@@ -113,24 +111,24 @@ export class BrowserMetricsAggregator {
|
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
hasData() {
|
|
116
|
-
return this.
|
|
117
|
-
this.pageLoads.length > 0 ||
|
|
114
|
+
return this.requests.size > 0 ||
|
|
118
115
|
Object.keys(this.webVitals).length > 0 ||
|
|
119
116
|
this.jsErrors > 0 ||
|
|
120
|
-
this.routeChanges.length > 0
|
|
117
|
+
this.routeChanges.length > 0 ||
|
|
118
|
+
this.pageLoads.length > 0;
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
/**
|
|
124
122
|
* Flush aggregated metrics and return payload for the backend.
|
|
125
123
|
* Payload format matches the backend's ingestApmData expectations:
|
|
126
|
-
* { interval, service, environment, requests[], dependencies[]
|
|
124
|
+
* { interval, service, environment, requests[], dependencies[] }
|
|
127
125
|
*/
|
|
128
126
|
flush() {
|
|
129
|
-
const
|
|
130
|
-
for (const metric of this.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
const requests = [];
|
|
128
|
+
for (const metric of this.requests.values()) {
|
|
129
|
+
requests.push({
|
|
130
|
+
method: metric.method,
|
|
131
|
+
route: metric.route,
|
|
134
132
|
count: metric.count,
|
|
135
133
|
errorCount: metric.errorCount,
|
|
136
134
|
latency: {
|
|
@@ -145,18 +143,8 @@ export class BrowserMetricsAggregator {
|
|
|
145
143
|
interval: `${config.flushInterval}s`,
|
|
146
144
|
service: config.service,
|
|
147
145
|
environment: config.environment,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
dependencies,
|
|
151
|
-
// Browser-specific metrics in a dedicated field.
|
|
152
|
-
// The backend safely ignores unknown fields for now;
|
|
153
|
-
// a future backend update can process these.
|
|
154
|
-
browser: {
|
|
155
|
-
pageLoads: [...this.pageLoads],
|
|
156
|
-
webVitals: { ...this.webVitals },
|
|
157
|
-
jsErrors: this.jsErrors,
|
|
158
|
-
routeChanges: [...this.routeChanges]
|
|
159
|
-
}
|
|
146
|
+
requests,
|
|
147
|
+
dependencies: []
|
|
160
148
|
};
|
|
161
149
|
|
|
162
150
|
this.reset();
|
|
@@ -165,11 +153,11 @@ export class BrowserMetricsAggregator {
|
|
|
165
153
|
|
|
166
154
|
getStats() {
|
|
167
155
|
return {
|
|
168
|
-
|
|
169
|
-
pageLoads: this.pageLoads.length,
|
|
156
|
+
requestMetrics: this.requests.size,
|
|
170
157
|
webVitals: Object.keys(this.webVitals).length,
|
|
171
158
|
jsErrors: this.jsErrors,
|
|
172
|
-
routeChanges: this.routeChanges.length
|
|
159
|
+
routeChanges: this.routeChanges.length,
|
|
160
|
+
pageLoads: this.pageLoads.length
|
|
173
161
|
};
|
|
174
162
|
}
|
|
175
163
|
}
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
* Browser Network Instrumentation
|
|
3
3
|
*
|
|
4
4
|
* Monkey-patches window.fetch and XMLHttpRequest to track outgoing HTTP requests.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Captured data is recorded as dependencies (same shape as Node.js SDK)
|
|
8
|
-
* so the backend processes them identically.
|
|
5
|
+
* Captured data is recorded as requests (method + pathname), matching the
|
|
6
|
+
* Node.js SDK's request format so the backend processes them identically.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import { getAggregator } from './aggregator.js';
|
|
@@ -33,17 +31,16 @@ function shouldExclude(url) {
|
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/**
|
|
36
|
-
* Extract
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Extract the pathname from a URL.
|
|
35
|
+
* e.g., "http://localhost:4005/api/todos/1" → "/api/todos/1"
|
|
36
|
+
* e.g., "/api/todos" → "/api/todos"
|
|
39
37
|
*/
|
|
40
|
-
function
|
|
38
|
+
function getPathname(url) {
|
|
41
39
|
try {
|
|
42
40
|
const parsed = new URL(url, window.location.origin);
|
|
43
|
-
|
|
44
|
-
return parsed.hostname;
|
|
41
|
+
return parsed.pathname;
|
|
45
42
|
} catch {
|
|
46
|
-
return 'unknown';
|
|
43
|
+
return '/unknown';
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
46
|
|
|
@@ -66,23 +63,23 @@ function instrumentFetch() {
|
|
|
66
63
|
|
|
67
64
|
const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
68
65
|
const startTime = performance.now();
|
|
69
|
-
const
|
|
66
|
+
const pathname = getPathname(url);
|
|
70
67
|
|
|
71
68
|
return originalFetch.apply(this, arguments).then(
|
|
72
69
|
(response) => {
|
|
73
70
|
const latency = performance.now() - startTime;
|
|
74
71
|
const isError = !response.ok;
|
|
75
72
|
|
|
76
|
-
getAggregator().
|
|
77
|
-
debug(`Fetch: ${method} ${
|
|
73
|
+
getAggregator().recordRequest(method, pathname, latency, isError);
|
|
74
|
+
debug(`Fetch: ${method} ${pathname} ${response.status} ${latency.toFixed(1)}ms`);
|
|
78
75
|
|
|
79
76
|
return response;
|
|
80
77
|
},
|
|
81
78
|
(error) => {
|
|
82
79
|
const latency = performance.now() - startTime;
|
|
83
80
|
|
|
84
|
-
getAggregator().
|
|
85
|
-
debug(`Fetch error: ${method} ${
|
|
81
|
+
getAggregator().recordRequest(method, pathname, latency, true);
|
|
82
|
+
debug(`Fetch error: ${method} ${pathname} ${latency.toFixed(1)}ms`);
|
|
86
83
|
|
|
87
84
|
throw error;
|
|
88
85
|
}
|
|
@@ -113,14 +110,14 @@ function instrumentXHR() {
|
|
|
113
110
|
const startTime = performance.now();
|
|
114
111
|
const url = this._sgUrl;
|
|
115
112
|
const method = this._sgMethod;
|
|
116
|
-
const
|
|
113
|
+
const pathname = getPathname(url);
|
|
117
114
|
|
|
118
115
|
this.addEventListener('loadend', function () {
|
|
119
116
|
const latency = performance.now() - startTime;
|
|
120
117
|
const isError = this.status === 0 || this.status >= 400;
|
|
121
118
|
|
|
122
|
-
getAggregator().
|
|
123
|
-
debug(`XHR: ${method} ${
|
|
119
|
+
getAggregator().recordRequest(method, pathname, latency, isError);
|
|
120
|
+
debug(`XHR: ${method} ${pathname} ${this.status} ${latency.toFixed(1)}ms`);
|
|
124
121
|
});
|
|
125
122
|
|
|
126
123
|
return originalXhrSend.apply(this, arguments);
|