@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentienguard/apm",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "SentienGuard APM SDK - Minimal, production-safe application performance monitoring",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * Browser Metrics Aggregator
3
3
  *
4
- * Batches browser-specific metrics in memory and flushes them periodically.
4
+ * Batches browser metrics in memory and flushes them periodically.
5
5
  * Produces payloads compatible with the backend's ingestApmData controller.
6
6
  *
7
- * Mirrors the Node.js aggregator pattern but tracks browser-relevant metrics:
8
- * - Outgoing fetch/XHR requests (as dependencies)
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 dependency metrics
18
+ * Create an aggregation key for request metrics
19
19
  */
20
- function createDependencyKey(name, type) {
21
- return `${name}:${type}`;
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 host
32
- // Same shape as Node SDK dependencies: { name, type, count, errorCount, latency }
33
- this.dependencies = new Map();
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} name - Service/host name
54
- * @param {string} type - Dependency type (http)
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
- recordDependency(name, type, latency, isError = false) {
59
- const key = createDependencyKey(name, type);
56
+ recordRequest(method, route, latency, isError = false) {
57
+ const key = createRequestKey(method, route);
60
58
 
61
- let metric = this.dependencies.get(key);
59
+ let metric = this.requests.get(key);
62
60
  if (!metric) {
63
- if (this.dependencies.size >= this.maxRoutes) return;
61
+ if (this.requests.size >= this.maxRoutes) return;
64
62
  metric = {
65
- name,
66
- type,
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.dependencies.set(key, metric);
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.dependencies.size > 0 ||
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[], browser{} }
124
+ * { interval, service, environment, requests[], dependencies[] }
127
125
  */
128
126
  flush() {
129
- const dependencies = [];
130
- for (const metric of this.dependencies.values()) {
131
- dependencies.push({
132
- name: metric.name,
133
- type: metric.type,
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
- // requests[] left empty - browser doesn't serve HTTP requests
149
- requests: [],
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
- dependencyMetrics: this.dependencies.size,
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
- * This is the industry-standard approach used by Datadog, Sentry, New Relic, and Elastic APM.
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 a service name from a URL.
37
- * For same-origin requests, returns 'self'.
38
- * For cross-origin, returns the hostname.
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 getServiceName(url) {
38
+ function getPathname(url) {
41
39
  try {
42
40
  const parsed = new URL(url, window.location.origin);
43
- if (parsed.origin === window.location.origin) return 'self';
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 serviceName = getServiceName(url);
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().recordDependency(serviceName, 'http', latency, isError);
77
- debug(`Fetch: ${method} ${serviceName} ${response.status} ${latency.toFixed(1)}ms`);
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().recordDependency(serviceName, 'http', latency, true);
85
- debug(`Fetch error: ${method} ${serviceName} ${latency.toFixed(1)}ms`);
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 serviceName = getServiceName(url);
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().recordDependency(serviceName, 'http', latency, isError);
123
- debug(`XHR: ${method} ${serviceName} ${this.status} ${latency.toFixed(1)}ms`);
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);