@sentienguard/apm 1.0.4 → 1.0.6
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/aggregator.js +463 -463
- package/src/browser/aggregator.js +38 -50
- package/src/browser/instrumentation.js +16 -19
- package/src/browser/transport.js +22 -21
- package/src/browser.js +3 -2
- package/src/circuitBreaker.js +264 -254
- package/src/dependencies.js +231 -236
- package/src/index.js +209 -209
- package/src/instrumentation.js +208 -208
- package/src/normalizer.js +147 -147
- package/src/transport.js +215 -214
|
@@ -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);
|
package/src/browser/transport.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Browser Transport Layer
|
|
3
3
|
*
|
|
4
4
|
* Handles periodic flush of aggregated metrics to the backend.
|
|
5
|
-
* Uses fetch() for normal sends (
|
|
6
|
-
* navigator.sendBeacon() for page unload (more reliable but no custom headers).
|
|
5
|
+
* Uses fetch() for normal sends and navigator.sendBeacon() for page unload.
|
|
7
6
|
*
|
|
8
|
-
* Transport strategy
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
7
|
+
* Transport strategy:
|
|
8
|
+
* - API key is always sent as ?apmKey= query param (avoids custom header CORS preflight issues)
|
|
9
|
+
* - Normal flush: fetch() with keepalive
|
|
10
|
+
* - Unload flush: sendBeacon() (more reliable on page close)
|
|
11
11
|
* - Listens on visibilitychange (NOT beforeunload/unload which are unreliable on mobile)
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -21,7 +21,16 @@ let consecutiveFailures = 0;
|
|
|
21
21
|
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Build the ingest URL with API key as query parameter.
|
|
25
|
+
* Using query param instead of X-APM-Key header avoids CORS preflight
|
|
26
|
+
* issues across different server/proxy configurations.
|
|
27
|
+
*/
|
|
28
|
+
function getIngestUrl() {
|
|
29
|
+
return `${config.endpoint}?apmKey=${encodeURIComponent(config.apiKey)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Send payload to backend using fetch
|
|
25
34
|
*/
|
|
26
35
|
async function sendToBackend(payload) {
|
|
27
36
|
const data = JSON.stringify(payload);
|
|
@@ -31,12 +40,9 @@ async function sendToBackend(payload) {
|
|
|
31
40
|
return;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
const response = await fetch(
|
|
43
|
+
const response = await fetch(getIngestUrl(), {
|
|
35
44
|
method: 'POST',
|
|
36
|
-
headers: {
|
|
37
|
-
'Content-Type': 'application/json',
|
|
38
|
-
'X-APM-Key': config.apiKey
|
|
39
|
-
},
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
46
|
body: data,
|
|
41
47
|
keepalive: true
|
|
42
48
|
});
|
|
@@ -48,26 +54,21 @@ async function sendToBackend(payload) {
|
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* Send payload using navigator.sendBeacon (for page unload).
|
|
51
|
-
* sendBeacon
|
|
52
|
-
* as a query parameter — same pattern as Datadog's clientToken.
|
|
57
|
+
* sendBeacon is more reliable than fetch during page close/navigation.
|
|
53
58
|
*/
|
|
54
59
|
function sendBeacon(payload) {
|
|
55
60
|
const data = JSON.stringify(payload);
|
|
56
61
|
|
|
57
62
|
if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
|
|
58
|
-
const url = `${config.endpoint}?apmKey=${encodeURIComponent(config.apiKey)}`;
|
|
59
63
|
const blob = new Blob([data], { type: 'application/json' });
|
|
60
|
-
return navigator.sendBeacon(
|
|
64
|
+
return navigator.sendBeacon(getIngestUrl(), blob);
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
// Fallback: fetch with keepalive (survives page unload in modern browsers)
|
|
64
68
|
try {
|
|
65
|
-
fetch(
|
|
69
|
+
fetch(getIngestUrl(), {
|
|
66
70
|
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
'X-APM-Key': config.apiKey
|
|
70
|
-
},
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
72
|
body: data,
|
|
72
73
|
keepalive: true
|
|
73
74
|
});
|
|
@@ -86,7 +87,7 @@ export async function flush() {
|
|
|
86
87
|
if (!aggregator.hasData()) return;
|
|
87
88
|
|
|
88
89
|
const payload = aggregator.flush();
|
|
89
|
-
debug(`Flushing ${payload.
|
|
90
|
+
debug(`Flushing ${payload.requests.length} request metrics`);
|
|
90
91
|
|
|
91
92
|
try {
|
|
92
93
|
const startTime = performance.now();
|
package/src/browser.js
CHANGED
|
@@ -130,8 +130,9 @@ function getStatus() {
|
|
|
130
130
|
const noop = () => {};
|
|
131
131
|
function normalizeRoute(route) { return route; }
|
|
132
132
|
function extractRoute(req) { return req?.url || '/'; }
|
|
133
|
-
|
|
134
|
-
function
|
|
133
|
+
|
|
134
|
+
function instrumentMongoDB() { return noop(); }
|
|
135
|
+
function instrumentOpenAI() { return noop(); }
|
|
135
136
|
function createBreaker(fn) { return fn; }
|
|
136
137
|
function wrapMongoOperation(fn) { return fn; }
|
|
137
138
|
function getBreakerStats() { return {}; }
|