midline-agent 0.3.0 → 0.4.1
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 +90 -5
- package/browser/package.json +8 -0
- package/dist/agent.d.ts +9 -1
- package/dist/agent.js +44 -68
- package/dist/browser/client.d.ts +59 -0
- package/dist/browser/client.js +608 -0
- package/dist/browser/index.d.ts +34 -0
- package/dist/browser/index.js +65 -0
- package/dist/browser/instrument.d.ts +39 -0
- package/dist/browser/instrument.js +217 -0
- package/dist/browser/transport.d.ts +43 -0
- package/dist/browser/transport.js +168 -0
- package/dist/browser/types.d.ts +94 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/version.d.ts +2 -0
- package/dist/browser/version.js +5 -0
- package/dist/browser/vitals.d.ts +16 -0
- package/dist/browser/vitals.js +135 -0
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +8 -7
- package/dist/config.js +12 -24
- package/dist/esm/browser/client.js +601 -0
- package/dist/esm/browser/index.js +52 -0
- package/dist/esm/browser/instrument.js +210 -0
- package/dist/esm/browser/transport.js +164 -0
- package/dist/esm/browser/types.js +1 -0
- package/dist/esm/browser/version.js +2 -0
- package/dist/esm/browser/vitals.js +132 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/redact.js +224 -0
- package/dist/esm/types.js +1 -0
- package/dist/redact.d.ts +3 -0
- package/dist/redact.js +12 -6
- package/dist/socket-transport.d.ts +58 -0
- package/dist/socket-transport.js +157 -0
- package/package.json +31 -4
- package/scripts/mark-esm.js +6 -0
- package/src/agent.ts +46 -73
- package/src/browser/client.ts +686 -0
- package/src/browser/index.ts +74 -0
- package/src/browser/instrument.ts +275 -0
- package/src/browser/transport.ts +184 -0
- package/src/browser/types.ts +105 -0
- package/src/browser/version.ts +2 -0
- package/src/browser/vitals.ts +149 -0
- package/src/config.ts +12 -23
- package/src/redact.ts +12 -6
- package/src/socket-transport.ts +188 -0
- package/test/agent.test.js +47 -51
- package/test/browser.test.js +328 -0
- package/test/console.test.js +11 -10
- package/test/helpers.js +54 -1
- package/test/middleware.test.js +5 -5
- package/test/proxy.test.js +4 -4
- package/tsconfig.esm.json +14 -0
- package/src/transport.ts +0 -125
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.observeVitals = observeVitals;
|
|
4
|
+
/** web.dev thresholds: at or under the first is good, over the second is poor. */
|
|
5
|
+
const THRESHOLDS = {
|
|
6
|
+
LCP: [2500, 4000],
|
|
7
|
+
INP: [200, 500],
|
|
8
|
+
CLS: [0.1, 0.25],
|
|
9
|
+
FCP: [1800, 3000],
|
|
10
|
+
TTFB: [800, 1800],
|
|
11
|
+
};
|
|
12
|
+
const rate = (name, value) => value <= THRESHOLDS[name][0] ? "good" : value <= THRESHOLDS[name][1] ? "needs-improvement" : "poor";
|
|
13
|
+
/**
|
|
14
|
+
* Core Web Vitals without a dependency, measured the way the web-vitals library
|
|
15
|
+
* does and reported once, when the page is first hidden (the last moment a
|
|
16
|
+
* report reliably gets out). Browsers that don't expose an entry type simply
|
|
17
|
+
* omit that metric: Safari has no LCP, INP or CLS.
|
|
18
|
+
*/
|
|
19
|
+
function observeVitals(win, onReport) {
|
|
20
|
+
const Observer = win.PerformanceObserver;
|
|
21
|
+
const supported = Observer?.supportedEntryTypes ?? [];
|
|
22
|
+
const perf = win.performance;
|
|
23
|
+
const doc = win.document;
|
|
24
|
+
const values = {};
|
|
25
|
+
const observers = [];
|
|
26
|
+
let activationStart = 0;
|
|
27
|
+
let navigationType;
|
|
28
|
+
try {
|
|
29
|
+
const navigation = perf?.getEntriesByType?.("navigation")?.[0];
|
|
30
|
+
if (navigation) {
|
|
31
|
+
activationStart = navigation.activationStart ?? 0;
|
|
32
|
+
navigationType = navigation.type;
|
|
33
|
+
if (typeof navigation.responseStart === "number" && navigation.responseStart > 0) {
|
|
34
|
+
values.TTFB = Math.max(0, navigation.responseStart - activationStart);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// no navigation timing
|
|
40
|
+
}
|
|
41
|
+
const observe = (type, handle, extra = {}) => {
|
|
42
|
+
if (!Observer || !supported.includes(type))
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
const observer = new Observer((list) => handle(list.getEntries()));
|
|
46
|
+
observer.observe({ type, buffered: true, ...extra });
|
|
47
|
+
observers.push({ observer, handle });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// unsupported option in this browser
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
observe("paint", (entries) => {
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (entry.name === "first-contentful-paint")
|
|
56
|
+
values.FCP = Math.max(0, entry.startTime - activationStart);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
observe("largest-contentful-paint", (entries) => {
|
|
60
|
+
const last = entries[entries.length - 1];
|
|
61
|
+
if (last)
|
|
62
|
+
values.LCP = Math.max(0, last.startTime - activationStart);
|
|
63
|
+
});
|
|
64
|
+
// CLS is the largest session window: shifts less than 1s apart, at most 5s long.
|
|
65
|
+
let windowValue = 0;
|
|
66
|
+
let windowStart = 0;
|
|
67
|
+
let windowLast = 0;
|
|
68
|
+
observe("layout-shift", (entries) => {
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.hadRecentInput || typeof entry.value !== "number")
|
|
71
|
+
continue;
|
|
72
|
+
if (windowValue && entry.startTime - windowLast < 1000 && entry.startTime - windowStart < 5000) {
|
|
73
|
+
windowValue += entry.value;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
windowValue = entry.value;
|
|
77
|
+
windowStart = entry.startTime;
|
|
78
|
+
}
|
|
79
|
+
windowLast = entry.startTime;
|
|
80
|
+
values.CLS = Math.max(values.CLS ?? 0, windowValue);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// INP: the slowest interaction, or near the 98th percentile on busy pages.
|
|
84
|
+
const interactions = new Map();
|
|
85
|
+
const recordInteractions = (entries) => {
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (!entry.interactionId)
|
|
88
|
+
continue;
|
|
89
|
+
interactions.set(entry.interactionId, Math.max(interactions.get(entry.interactionId) ?? 0, entry.duration));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
observe("event", recordInteractions, { durationThreshold: 40 });
|
|
93
|
+
observe("first-input", recordInteractions);
|
|
94
|
+
let reported = false;
|
|
95
|
+
const report = () => {
|
|
96
|
+
if (reported)
|
|
97
|
+
return;
|
|
98
|
+
reported = true;
|
|
99
|
+
for (const { observer, handle } of observers) {
|
|
100
|
+
try {
|
|
101
|
+
handle(observer.takeRecords());
|
|
102
|
+
observer.disconnect();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// already disconnected
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (interactions.size) {
|
|
109
|
+
const durations = [...interactions.values()].sort((a, b) => b - a);
|
|
110
|
+
values.INP = durations[Math.min(durations.length - 1, Math.floor(durations.length / 50))];
|
|
111
|
+
}
|
|
112
|
+
const vitals = {};
|
|
113
|
+
for (const name of Object.keys(values)) {
|
|
114
|
+
const raw = values[name];
|
|
115
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
116
|
+
continue;
|
|
117
|
+
const value = name === "CLS" ? Math.round(raw * 10000) / 10000 : Math.round(raw);
|
|
118
|
+
vitals[name] = { value, rating: rate(name, value) };
|
|
119
|
+
}
|
|
120
|
+
if (Object.keys(vitals).length)
|
|
121
|
+
onReport({ vitals, navigationType });
|
|
122
|
+
};
|
|
123
|
+
const onVisibility = () => {
|
|
124
|
+
if (doc?.visibilityState === "hidden")
|
|
125
|
+
report();
|
|
126
|
+
};
|
|
127
|
+
doc?.addEventListener("visibilitychange", onVisibility, true);
|
|
128
|
+
win.addEventListener("pagehide", report, true);
|
|
129
|
+
return () => {
|
|
130
|
+
doc?.removeEventListener("visibilitychange", onVisibility, true);
|
|
131
|
+
win.removeEventListener("pagehide", report, true);
|
|
132
|
+
for (const { observer } of observers)
|
|
133
|
+
observer.disconnect();
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CaInput, CaptureOptions, MidlineConfig } from "./types";
|
|
2
2
|
export declare const DEFAULT_ENDPOINT = "https://api.usemidline.com";
|
|
3
|
-
export declare const INGEST_PATH = "/api/api-monitor/events";
|
|
4
3
|
/** A setting that cannot work as given. Thrown at construction, never while serving traffic. */
|
|
5
4
|
export declare class ConfigError extends Error {
|
|
6
5
|
constructor(message: string);
|
|
@@ -15,8 +14,8 @@ export interface ResolvedCapture {
|
|
|
15
14
|
export interface ResolvedConfig {
|
|
16
15
|
apiKey: string;
|
|
17
16
|
serviceName?: string;
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/** The Midline server's origin; the ingest gateway lives at `${socketOrigin}/ingest`. */
|
|
18
|
+
socketOrigin: URL;
|
|
20
19
|
/** Full trust store for the Midline endpoint, or undefined for Node's default. */
|
|
21
20
|
ca?: Array<string | Buffer>;
|
|
22
21
|
hasCustomCa: boolean;
|
|
@@ -43,12 +42,14 @@ export declare function envFlag(name: string): boolean | undefined;
|
|
|
43
42
|
export declare function envInt(name: string): number | undefined;
|
|
44
43
|
export declare function isLoopback(hostname: string): boolean;
|
|
45
44
|
/**
|
|
46
|
-
* Turns whatever the user configured into the
|
|
47
|
-
*
|
|
45
|
+
* Turns whatever the user configured into the Midline server's origin — the
|
|
46
|
+
* agent connects to `${origin}/ingest` over socket.io, so only the origin
|
|
47
|
+
* matters now. Tolerates an old-style full ingest URL some existing
|
|
48
|
+
* `MIDLINE_ENDPOINT` values still carry (from before the socket.io transport):
|
|
48
49
|
* `https://api.usemidline.com`, `https://api.usemidline.com/api/api-monitor/events`
|
|
49
|
-
* and `https://gateway.internal/midline` all
|
|
50
|
+
* and `https://gateway.internal/midline` all resolve to the same origin.
|
|
50
51
|
*/
|
|
51
|
-
export declare function
|
|
52
|
+
export declare function resolveEndpointOrigin(endpoint: string): URL;
|
|
52
53
|
/**
|
|
53
54
|
* Loads extra CAs. Every entry must contain at least one parseable certificate, so a
|
|
54
55
|
* typo in a path fails loudly at startup instead of silently trusting nothing.
|
package/dist/config.js
CHANGED
|
@@ -33,12 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ConfigError = exports.
|
|
36
|
+
exports.ConfigError = exports.DEFAULT_ENDPOINT = void 0;
|
|
37
37
|
exports.env = env;
|
|
38
38
|
exports.envFlag = envFlag;
|
|
39
39
|
exports.envInt = envInt;
|
|
40
40
|
exports.isLoopback = isLoopback;
|
|
41
|
-
exports.
|
|
41
|
+
exports.resolveEndpointOrigin = resolveEndpointOrigin;
|
|
42
42
|
exports.loadCa = loadCa;
|
|
43
43
|
exports.trustStore = trustStore;
|
|
44
44
|
exports.resolveCapture = resolveCapture;
|
|
@@ -47,7 +47,6 @@ const fs_1 = require("fs");
|
|
|
47
47
|
const crypto_1 = require("crypto");
|
|
48
48
|
const tls = __importStar(require("tls"));
|
|
49
49
|
exports.DEFAULT_ENDPOINT = "https://api.usemidline.com";
|
|
50
|
-
exports.INGEST_PATH = "/api/api-monitor/events";
|
|
51
50
|
/** A setting that cannot work as given. Thrown at construction, never while serving traffic. */
|
|
52
51
|
class ConfigError extends Error {
|
|
53
52
|
constructor(message) {
|
|
@@ -88,12 +87,14 @@ function isLoopback(hostname) {
|
|
|
88
87
|
/^127(\.\d{1,3}){3}$/.test(host));
|
|
89
88
|
}
|
|
90
89
|
/**
|
|
91
|
-
* Turns whatever the user configured into the
|
|
92
|
-
*
|
|
90
|
+
* Turns whatever the user configured into the Midline server's origin — the
|
|
91
|
+
* agent connects to `${origin}/ingest` over socket.io, so only the origin
|
|
92
|
+
* matters now. Tolerates an old-style full ingest URL some existing
|
|
93
|
+
* `MIDLINE_ENDPOINT` values still carry (from before the socket.io transport):
|
|
93
94
|
* `https://api.usemidline.com`, `https://api.usemidline.com/api/api-monitor/events`
|
|
94
|
-
* and `https://gateway.internal/midline` all
|
|
95
|
+
* and `https://gateway.internal/midline` all resolve to the same origin.
|
|
95
96
|
*/
|
|
96
|
-
function
|
|
97
|
+
function resolveEndpointOrigin(endpoint) {
|
|
97
98
|
let url;
|
|
98
99
|
try {
|
|
99
100
|
url = new URL(endpoint);
|
|
@@ -111,19 +112,7 @@ function resolveIngestUrl(endpoint) {
|
|
|
111
112
|
if (url.username || url.password) {
|
|
112
113
|
throw new ConfigError("endpoint must not contain credentials; pass the key as apiKey");
|
|
113
114
|
}
|
|
114
|
-
url.
|
|
115
|
-
url.hash = "";
|
|
116
|
-
const path = url.pathname.replace(/\/+$/, "");
|
|
117
|
-
if (path.endsWith(`${exports.INGEST_PATH}/batch`)) {
|
|
118
|
-
url.pathname = path.slice(0, -"/batch".length);
|
|
119
|
-
}
|
|
120
|
-
else if (path.endsWith(exports.INGEST_PATH)) {
|
|
121
|
-
url.pathname = path;
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
url.pathname = `${path}${exports.INGEST_PATH}`;
|
|
125
|
-
}
|
|
126
|
-
return url;
|
|
115
|
+
return new URL(url.origin);
|
|
127
116
|
}
|
|
128
117
|
/**
|
|
129
118
|
* Loads extra CAs. Every entry must contain at least one parseable certificate, so a
|
|
@@ -197,16 +186,15 @@ function resolveCapture(capture) {
|
|
|
197
186
|
}
|
|
198
187
|
function resolveConfig(config) {
|
|
199
188
|
const apiKey = (config.apiKey ?? env("MIDLINE_API_KEY") ?? "").trim();
|
|
200
|
-
const
|
|
189
|
+
const socketOrigin = resolveEndpointOrigin(config.endpoint || env("MIDLINE_ENDPOINT") || exports.DEFAULT_ENDPOINT);
|
|
201
190
|
const extraCa = loadCa(config.ca ?? env("MIDLINE_CUSTOM_CA"), "ca / MIDLINE_CUSTOM_CA");
|
|
202
|
-
if (extraCa &&
|
|
191
|
+
if (extraCa && socketOrigin.protocol !== "https:") {
|
|
203
192
|
throw new ConfigError("ca / MIDLINE_CUSTOM_CA is set but the endpoint is not https://");
|
|
204
193
|
}
|
|
205
194
|
return {
|
|
206
195
|
apiKey,
|
|
207
196
|
serviceName: config.serviceName ?? env("MIDLINE_SERVICE_NAME"),
|
|
208
|
-
|
|
209
|
-
batchUrl: new URL(`${ingestUrl.pathname}/batch`, ingestUrl),
|
|
197
|
+
socketOrigin,
|
|
210
198
|
ca: trustStore(extraCa),
|
|
211
199
|
hasCustomCa: Boolean(extraCa),
|
|
212
200
|
environment: config.environment ?? env("MIDLINE_ENVIRONMENT"),
|