margin-ts 0.6.1 → 0.7.0
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/dist/adapters/nextjs.d.ts +65 -0
- package/dist/adapters/nextjs.d.ts.map +1 -0
- package/dist/adapters/nextjs.js +233 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/stream.d.ts +77 -0
- package/dist/adapters/stream.d.ts.map +1 -0
- package/dist/adapters/stream.js +197 -0
- package/dist/adapters/stream.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/observation.d.ts +15 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +58 -2
- package/dist/observation.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/nextjs.ts +290 -0
- package/src/adapters/stream.ts +241 -0
- package/src/index.ts +17 -1
- package/src/observation.ts +56 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Copyright (c) 2026 Cope Labs LLC. MIT License.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.DEFAULT_SUITE_THRESHOLDS = exports.suiteHealthString = exports.classifySuite = exports.EXPRESS_THRESHOLDS = exports.marginHealthRoute = exports.marginMiddleware = exports.classifyAnomaly = exports.ANOMALY_SEVERITY = exports.AnomalyState = exports.classifyDrift = exports.DriftDirection = exports.DriftState = exports.Parser = exports.expressionToDict = exports.expressionToString = exports.degraded = exports.healthOf = exports.createExpression = exports.correctionIsActive = exports.observationFromDict = exports.observationToDict = exports.observationToAtom = exports.observationSigma = exports.Op = exports.classify = exports.isAblated = exports.isIntact = exports.createThresholds = exports.SEVERITY = exports.Health = exports.minConfidence = exports.confidenceLt = exports.confidenceGte = exports.confidenceRank = exports.Confidence = void 0;
|
|
11
|
+
exports.marginPoll = exports.marginSSEHandler = exports.marginSSE = exports.broadcast = exports.StreamingMonitor = exports.DEFAULT_NEXTJS_THRESHOLDS = exports.resetRoutes = exports.marginHealthAppHandler = exports.marginHealthHandler = exports.withMarginApp = exports.withMargin = exports.DEFAULT_SUITE_THRESHOLDS = exports.suiteHealthString = exports.classifySuite = exports.EXPRESS_THRESHOLDS = exports.marginHealthRoute = exports.marginMiddleware = exports.classifyAnomaly = exports.ANOMALY_SEVERITY = exports.AnomalyState = exports.classifyDrift = exports.DriftDirection = exports.DriftState = exports.Parser = exports.expressionToDict = exports.expressionToString = exports.intact = exports.absent = exports.degraded = exports.healthOf = exports.createExpression = exports.correctionIsActive = exports.observationFromDict = exports.observationToDict = exports.observationToAtom = exports.observationSigma = exports.isAbsent = exports.Op = exports.Absence = exports.classify = exports.isAblated = exports.isIntact = exports.createThresholds = exports.SEVERITY = exports.Health = exports.minConfidence = exports.confidenceLt = exports.confidenceGte = exports.confidenceRank = exports.Confidence = void 0;
|
|
12
|
+
exports.resetClients = exports.connectedClients = void 0;
|
|
12
13
|
var confidence_js_1 = require("./confidence.js");
|
|
13
14
|
Object.defineProperty(exports, "Confidence", { enumerable: true, get: function () { return confidence_js_1.Confidence; } });
|
|
14
15
|
Object.defineProperty(exports, "confidenceRank", { enumerable: true, get: function () { return confidence_js_1.confidenceRank; } });
|
|
@@ -23,7 +24,9 @@ Object.defineProperty(exports, "isIntact", { enumerable: true, get: function ()
|
|
|
23
24
|
Object.defineProperty(exports, "isAblated", { enumerable: true, get: function () { return health_js_1.isAblated; } });
|
|
24
25
|
Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return health_js_1.classify; } });
|
|
25
26
|
var observation_js_1 = require("./observation.js");
|
|
27
|
+
Object.defineProperty(exports, "Absence", { enumerable: true, get: function () { return observation_js_1.Absence; } });
|
|
26
28
|
Object.defineProperty(exports, "Op", { enumerable: true, get: function () { return observation_js_1.Op; } });
|
|
29
|
+
Object.defineProperty(exports, "isAbsent", { enumerable: true, get: function () { return observation_js_1.isAbsent; } });
|
|
27
30
|
Object.defineProperty(exports, "observationSigma", { enumerable: true, get: function () { return observation_js_1.observationSigma; } });
|
|
28
31
|
Object.defineProperty(exports, "observationToAtom", { enumerable: true, get: function () { return observation_js_1.observationToAtom; } });
|
|
29
32
|
Object.defineProperty(exports, "observationToDict", { enumerable: true, get: function () { return observation_js_1.observationToDict; } });
|
|
@@ -32,6 +35,8 @@ Object.defineProperty(exports, "correctionIsActive", { enumerable: true, get: fu
|
|
|
32
35
|
Object.defineProperty(exports, "createExpression", { enumerable: true, get: function () { return observation_js_1.createExpression; } });
|
|
33
36
|
Object.defineProperty(exports, "healthOf", { enumerable: true, get: function () { return observation_js_1.healthOf; } });
|
|
34
37
|
Object.defineProperty(exports, "degraded", { enumerable: true, get: function () { return observation_js_1.degraded; } });
|
|
38
|
+
Object.defineProperty(exports, "absent", { enumerable: true, get: function () { return observation_js_1.absent; } });
|
|
39
|
+
Object.defineProperty(exports, "intact", { enumerable: true, get: function () { return observation_js_1.intact; } });
|
|
35
40
|
Object.defineProperty(exports, "expressionToString", { enumerable: true, get: function () { return observation_js_1.expressionToString; } });
|
|
36
41
|
Object.defineProperty(exports, "expressionToDict", { enumerable: true, get: function () { return observation_js_1.expressionToDict; } });
|
|
37
42
|
Object.defineProperty(exports, "Parser", { enumerable: true, get: function () { return observation_js_1.Parser; } });
|
|
@@ -52,4 +57,19 @@ var vitest_js_1 = require("./adapters/vitest.js");
|
|
|
52
57
|
Object.defineProperty(exports, "classifySuite", { enumerable: true, get: function () { return vitest_js_1.classifySuite; } });
|
|
53
58
|
Object.defineProperty(exports, "suiteHealthString", { enumerable: true, get: function () { return vitest_js_1.suiteHealthString; } });
|
|
54
59
|
Object.defineProperty(exports, "DEFAULT_SUITE_THRESHOLDS", { enumerable: true, get: function () { return vitest_js_1.DEFAULT_SUITE_THRESHOLDS; } });
|
|
60
|
+
var nextjs_js_1 = require("./adapters/nextjs.js");
|
|
61
|
+
Object.defineProperty(exports, "withMargin", { enumerable: true, get: function () { return nextjs_js_1.withMargin; } });
|
|
62
|
+
Object.defineProperty(exports, "withMarginApp", { enumerable: true, get: function () { return nextjs_js_1.withMarginApp; } });
|
|
63
|
+
Object.defineProperty(exports, "marginHealthHandler", { enumerable: true, get: function () { return nextjs_js_1.marginHealthHandler; } });
|
|
64
|
+
Object.defineProperty(exports, "marginHealthAppHandler", { enumerable: true, get: function () { return nextjs_js_1.marginHealthAppHandler; } });
|
|
65
|
+
Object.defineProperty(exports, "resetRoutes", { enumerable: true, get: function () { return nextjs_js_1.resetRoutes; } });
|
|
66
|
+
Object.defineProperty(exports, "DEFAULT_NEXTJS_THRESHOLDS", { enumerable: true, get: function () { return nextjs_js_1.DEFAULT_NEXTJS_THRESHOLDS; } });
|
|
67
|
+
var stream_js_1 = require("./adapters/stream.js");
|
|
68
|
+
Object.defineProperty(exports, "StreamingMonitor", { enumerable: true, get: function () { return stream_js_1.StreamingMonitor; } });
|
|
69
|
+
Object.defineProperty(exports, "broadcast", { enumerable: true, get: function () { return stream_js_1.broadcast; } });
|
|
70
|
+
Object.defineProperty(exports, "marginSSE", { enumerable: true, get: function () { return stream_js_1.marginSSE; } });
|
|
71
|
+
Object.defineProperty(exports, "marginSSEHandler", { enumerable: true, get: function () { return stream_js_1.marginSSEHandler; } });
|
|
72
|
+
Object.defineProperty(exports, "marginPoll", { enumerable: true, get: function () { return stream_js_1.marginPoll; } });
|
|
73
|
+
Object.defineProperty(exports, "connectedClients", { enumerable: true, get: function () { return stream_js_1.connectedClients; } });
|
|
74
|
+
Object.defineProperty(exports, "resetClients", { enumerable: true, get: function () { return stream_js_1.resetClients; } });
|
|
55
75
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;;AAEH,iDAAyG;AAAhG,2GAAA,UAAU,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,8GAAA,aAAa,OAAA;AAAE,6GAAA,YAAY,OAAA;AAAE,8GAAA,aAAa,OAAA;AAE/E,yCAGqB;AAFnB,mGAAA,MAAM,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAAc,6GAAA,gBAAgB,OAAA;AAC9C,qGAAA,QAAQ,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAG/B,mDAQ0B;AAPxB,yGAAA,OAAO,OAAA;AACP,oGAAA,EAAE,OAAA;AACF,0GAAA,QAAQ,OAAA;AACR,kHAAA,gBAAgB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,qHAAA,mBAAmB,OAAA;AAC3E,oHAAA,kBAAkB,OAAA;AAClB,kHAAA,gBAAgB,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,wGAAA,MAAM,OAAA;AAAE,wGAAA,MAAM,OAAA;AAAE,oHAAA,kBAAkB,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAC1F,wGAAA,MAAM,OAAA;AAGR,uCAGoB;AAFlB,sGAAA,UAAU,OAAA;AAAE,0GAAA,cAAc,OAAA;AAC1B,yGAAA,aAAa,OAAA;AAGf,2CAGsB;AAFpB,0GAAA,YAAY,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAC9B,6GAAA,eAAe,OAAA;AAGjB,WAAW;AACX,oDAI+B;AAH7B,8GAAA,gBAAgB,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AACnC,gHAAA,kBAAkB,OAAsB;AAI1C,kDAI8B;AAH5B,0GAAA,aAAa,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAChC,qHAAA,wBAAwB,OAAA;AAI1B,kDAM8B;AAL5B,uGAAA,UAAU,OAAA;AAAE,0GAAA,aAAa,OAAA;AACzB,gHAAA,mBAAmB,OAAA;AAAE,mHAAA,sBAAsB,OAAA;AAC3C,wGAAA,WAAW,OAAA;AACX,sHAAA,yBAAyB,OAAA;AAI3B,kDAI8B;AAH5B,6GAAA,gBAAgB,OAAA;AAChB,sGAAA,SAAS,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,6GAAA,gBAAgB,OAAA;AAAE,uGAAA,UAAU,OAAA;AAClD,6GAAA,gBAAgB,OAAA;AAAE,yGAAA,YAAY,OAAA"}
|
package/dist/observation.d.ts
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Confidence } from './confidence.js';
|
|
5
5
|
import { Health, Thresholds } from './health.js';
|
|
6
|
+
export declare enum Absence {
|
|
7
|
+
NOT_MEASURED = "not_measured",
|
|
8
|
+
BELOW_DETECTION = "below_detection",
|
|
9
|
+
ABOVE_RANGE = "above_range",
|
|
10
|
+
SENSOR_FAILED = "sensor_failed",
|
|
11
|
+
REDACTED = "redacted",
|
|
12
|
+
NOT_APPLICABLE = "not_applicable",
|
|
13
|
+
PENDING = "pending"
|
|
14
|
+
}
|
|
6
15
|
export interface Observation {
|
|
7
16
|
name: string;
|
|
8
17
|
health: Health;
|
|
@@ -12,7 +21,10 @@ export interface Observation {
|
|
|
12
21
|
higherIsBetter: boolean;
|
|
13
22
|
provenance: string[];
|
|
14
23
|
measuredAt?: Date;
|
|
24
|
+
absence?: Absence;
|
|
25
|
+
absenceDetail?: string;
|
|
15
26
|
}
|
|
27
|
+
export declare function isAbsent(obs: Observation): boolean;
|
|
16
28
|
export declare function observationSigma(obs: Observation): number;
|
|
17
29
|
export declare function observationToAtom(obs: Observation): string;
|
|
18
30
|
export declare function observationToDict(obs: Observation): Record<string, unknown>;
|
|
@@ -42,6 +54,8 @@ export interface Expression {
|
|
|
42
54
|
export declare function createExpression(observations?: Observation[], corrections?: Correction[], label?: string, step?: number): Expression;
|
|
43
55
|
export declare function healthOf(expr: Expression, name: string): Health | undefined;
|
|
44
56
|
export declare function degraded(expr: Expression): Observation[];
|
|
57
|
+
export declare function absent(expr: Expression): Observation[];
|
|
58
|
+
export declare function intact(expr: Expression): Observation[];
|
|
45
59
|
export declare function expressionToString(expr: Expression): string;
|
|
46
60
|
export declare function expressionToDict(expr: Expression): Record<string, unknown>;
|
|
47
61
|
export declare class Parser {
|
|
@@ -54,6 +68,7 @@ export declare class Parser {
|
|
|
54
68
|
label?: string;
|
|
55
69
|
step?: number;
|
|
56
70
|
confidences?: Record<string, Confidence>;
|
|
71
|
+
absences?: Record<string, Absence>;
|
|
57
72
|
}): Expression;
|
|
58
73
|
}
|
|
59
74
|
//# sourceMappingURL=observation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAiB,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,UAAU,EAAkD,MAAM,aAAa,CAAC;AAMjG,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"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"}
|
package/dist/observation.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Observations, corrections, expressions, and parser.
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.Parser = exports.Op = void 0;
|
|
6
|
+
exports.Parser = exports.Op = exports.Absence = void 0;
|
|
7
|
+
exports.isAbsent = isAbsent;
|
|
7
8
|
exports.observationSigma = observationSigma;
|
|
8
9
|
exports.observationToAtom = observationToAtom;
|
|
9
10
|
exports.observationToDict = observationToDict;
|
|
@@ -12,10 +13,28 @@ exports.correctionIsActive = correctionIsActive;
|
|
|
12
13
|
exports.createExpression = createExpression;
|
|
13
14
|
exports.healthOf = healthOf;
|
|
14
15
|
exports.degraded = degraded;
|
|
16
|
+
exports.absent = absent;
|
|
17
|
+
exports.intact = intact;
|
|
15
18
|
exports.expressionToString = expressionToString;
|
|
16
19
|
exports.expressionToDict = expressionToDict;
|
|
17
20
|
const confidence_js_1 = require("./confidence.js");
|
|
18
21
|
const health_js_1 = require("./health.js");
|
|
22
|
+
// -----------------------------------------------------------------------
|
|
23
|
+
// Absence — why a value is missing
|
|
24
|
+
// -----------------------------------------------------------------------
|
|
25
|
+
var Absence;
|
|
26
|
+
(function (Absence) {
|
|
27
|
+
Absence["NOT_MEASURED"] = "not_measured";
|
|
28
|
+
Absence["BELOW_DETECTION"] = "below_detection";
|
|
29
|
+
Absence["ABOVE_RANGE"] = "above_range";
|
|
30
|
+
Absence["SENSOR_FAILED"] = "sensor_failed";
|
|
31
|
+
Absence["REDACTED"] = "redacted";
|
|
32
|
+
Absence["NOT_APPLICABLE"] = "not_applicable";
|
|
33
|
+
Absence["PENDING"] = "pending";
|
|
34
|
+
})(Absence || (exports.Absence = Absence = {}));
|
|
35
|
+
function isAbsent(obs) {
|
|
36
|
+
return obs.absence !== undefined;
|
|
37
|
+
}
|
|
19
38
|
function observationSigma(obs) {
|
|
20
39
|
if (obs.baseline === 0)
|
|
21
40
|
return 0;
|
|
@@ -23,6 +42,8 @@ function observationSigma(obs) {
|
|
|
23
42
|
return obs.higherIsBetter ? raw : -raw;
|
|
24
43
|
}
|
|
25
44
|
function observationToAtom(obs) {
|
|
45
|
+
if (obs.absence !== undefined)
|
|
46
|
+
return `${obs.name}:ABSENT(${obs.absence})`;
|
|
26
47
|
if (obs.health === health_js_1.Health.OOD)
|
|
27
48
|
return `${obs.name}:${obs.health}`;
|
|
28
49
|
const sigma = observationSigma(obs);
|
|
@@ -42,10 +63,14 @@ function observationToDict(obs) {
|
|
|
42
63
|
};
|
|
43
64
|
if (obs.measuredAt)
|
|
44
65
|
d.measuredAt = obs.measuredAt.toISOString();
|
|
66
|
+
if (obs.absence !== undefined)
|
|
67
|
+
d.absence = obs.absence;
|
|
68
|
+
if (obs.absenceDetail !== undefined)
|
|
69
|
+
d.absenceDetail = obs.absenceDetail;
|
|
45
70
|
return d;
|
|
46
71
|
}
|
|
47
72
|
function observationFromDict(d) {
|
|
48
|
-
|
|
73
|
+
const obs = {
|
|
49
74
|
name: d.name,
|
|
50
75
|
health: d.health,
|
|
51
76
|
value: d.value,
|
|
@@ -55,6 +80,11 @@ function observationFromDict(d) {
|
|
|
55
80
|
provenance: d.provenance ?? [],
|
|
56
81
|
measuredAt: d.measuredAt ? new Date(d.measuredAt) : undefined,
|
|
57
82
|
};
|
|
83
|
+
if (d.absence !== undefined)
|
|
84
|
+
obs.absence = d.absence;
|
|
85
|
+
if (d.absenceDetail !== undefined)
|
|
86
|
+
obs.absenceDetail = d.absenceDetail;
|
|
87
|
+
return obs;
|
|
58
88
|
}
|
|
59
89
|
// -----------------------------------------------------------------------
|
|
60
90
|
// Correction
|
|
@@ -81,6 +111,12 @@ function healthOf(expr, name) {
|
|
|
81
111
|
function degraded(expr) {
|
|
82
112
|
return expr.observations.filter(o => o.health === health_js_1.Health.DEGRADED || o.health === health_js_1.Health.ABLATED || o.health === health_js_1.Health.RECOVERING);
|
|
83
113
|
}
|
|
114
|
+
function absent(expr) {
|
|
115
|
+
return expr.observations.filter(o => isAbsent(o));
|
|
116
|
+
}
|
|
117
|
+
function intact(expr) {
|
|
118
|
+
return expr.observations.filter(o => o.health === health_js_1.Health.INTACT);
|
|
119
|
+
}
|
|
84
120
|
function expressionToString(expr) {
|
|
85
121
|
if (expr.observations.length === 0)
|
|
86
122
|
return '[∅]';
|
|
@@ -123,8 +159,11 @@ class Parser {
|
|
|
123
159
|
}
|
|
124
160
|
parse(values, options = {}) {
|
|
125
161
|
const confidences = options.confidences ?? {};
|
|
162
|
+
const absences = options.absences ?? {};
|
|
126
163
|
const observations = [];
|
|
127
164
|
for (const [name, val] of Object.entries(values)) {
|
|
165
|
+
if (name in absences)
|
|
166
|
+
continue; // handled below
|
|
128
167
|
const baseline = this.baselines[name] ?? val;
|
|
129
168
|
const conf = confidences[name] ?? confidence_js_1.Confidence.MODERATE;
|
|
130
169
|
const t = this.thresholdsFor(name);
|
|
@@ -140,6 +179,23 @@ class Parser {
|
|
|
140
179
|
measuredAt: undefined,
|
|
141
180
|
});
|
|
142
181
|
}
|
|
182
|
+
// Emit absent observations
|
|
183
|
+
for (const [name, reason] of Object.entries(absences)) {
|
|
184
|
+
const baseline = this.baselines[name] ?? 0;
|
|
185
|
+
const t = this.thresholdsFor(name);
|
|
186
|
+
const conf = confidences[name] ?? confidence_js_1.Confidence.INDETERMINATE;
|
|
187
|
+
observations.push({
|
|
188
|
+
name,
|
|
189
|
+
health: health_js_1.Health.OOD,
|
|
190
|
+
value: baseline,
|
|
191
|
+
baseline,
|
|
192
|
+
confidence: conf,
|
|
193
|
+
higherIsBetter: t.higherIsBetter !== false,
|
|
194
|
+
provenance: [],
|
|
195
|
+
measuredAt: undefined,
|
|
196
|
+
absence: reason,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
143
199
|
return createExpression(observations, [], options.label ?? '', options.step);
|
|
144
200
|
}
|
|
145
201
|
}
|
package/dist/observation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observation.js","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":";AAAA;;GAEG;;;
|
|
1
|
+
{"version":3,"file":"observation.js","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAoCH,4BAEC;AAED,4CAIC;AAED,8CAMC;AAED,8CAeC;AAED,kDAcC;AAsBD,gDAEC;AAcD,4CAUC;AAED,4BAEC;AAED,4BAIC;AAED,wBAEC;AAED,wBAEC;AAED,gDAWC;AAED,4CAWC;AA/KD,mDAA4D;AAC5D,2CAAiG;AAEjG,0EAA0E;AAC1E,mCAAmC;AACnC,0EAA0E;AAE1E,IAAY,OAQX;AARD,WAAY,OAAO;IACjB,wCAA6B,CAAA;IAC7B,8CAAmC,CAAA;IACnC,sCAA2B,CAAA;IAC3B,0CAA+B,CAAA;IAC/B,gCAAqB,CAAA;IACrB,4CAAiC,CAAA;IACjC,8BAAmB,CAAA;AACrB,CAAC,EARW,OAAO,uBAAP,OAAO,QAQlB;AAmBD,SAAgB,QAAQ,CAAC,GAAgB;IACvC,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC;AACnC,CAAC;AAED,SAAgB,gBAAgB,CAAC,GAAgB;IAC/C,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAChE,OAAO,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACzC,CAAC;AAED,SAAgB,iBAAiB,CAAC,GAAgB;IAChD,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,GAAG,GAAG,CAAC,IAAI,WAAW,GAAG,CAAC,OAAO,GAAG,CAAC;IAC3E,IAAI,GAAG,CAAC,MAAM,KAAK,kBAAM,CAAC,GAAG;QAAE,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;IAClE,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACnC,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAClE,CAAC;AAED,SAAgB,iBAAiB,CAAC,GAAgB;IAChD,MAAM,CAAC,GAA4B;QACjC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,KAAK,EAAE,gBAAgB,CAAC,GAAG,CAAC;QAC5B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,UAAU,EAAE,GAAG,CAAC,UAAU;KAC3B,CAAC;IACF,IAAI,GAAG,CAAC,UAAU;QAAE,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IAChE,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS;QAAE,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IACvD,IAAI,GAAG,CAAC,aAAa,KAAK,SAAS;QAAE,CAAC,CAAC,aAAa,GAAG,GAAG,CAAC,aAAa,CAAC;IACzE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAgB,mBAAmB,CAAC,CAA0B;IAC5D,MAAM,GAAG,GAAgB;QACvB,IAAI,EAAE,CAAC,CAAC,IAAc;QACtB,MAAM,EAAE,CAAC,CAAC,MAAgB;QAC1B,KAAK,EAAE,CAAC,CAAC,KAAe;QACxB,QAAQ,EAAE,CAAC,CAAC,QAAkB;QAC9B,UAAU,EAAE,CAAC,CAAC,UAAwB;QACtC,cAAc,EAAG,CAAC,CAAC,cAA0B,IAAI,IAAI;QACrD,UAAU,EAAG,CAAC,CAAC,UAAuB,IAAI,EAAE;QAC5C,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,UAAoB,CAAC,CAAC,CAAC,CAAC,SAAS;KACxE,CAAC;IACF,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS;QAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,OAAkB,CAAC;IAChE,IAAI,CAAC,CAAC,aAAa,KAAK,SAAS;QAAE,GAAG,CAAC,aAAa,GAAG,CAAC,CAAC,aAAuB,CAAC;IACjF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,0EAA0E;AAC1E,aAAa;AACb,0EAA0E;AAE1E,IAAY,EAKX;AALD,WAAY,EAAE;IACZ,yBAAmB,CAAA;IACnB,2BAAqB,CAAA;IACrB,yBAAmB,CAAA;IACnB,mBAAa,CAAA;AACf,CAAC,EALW,EAAE,kBAAF,EAAE,QAKb;AAWD,SAAgB,kBAAkB,CAAC,CAAa;IAC9C,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;AACzC,CAAC;AAcD,SAAgB,gBAAgB,CAC9B,eAA8B,EAAE,EAChC,cAA4B,EAAE,EAC9B,KAAK,GAAG,EAAE,EACV,IAAa;IAEb,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC;QACxC,CAAC,CAAC,IAAA,6BAAa,EAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvD,CAAC,CAAC,0BAAU,CAAC,aAAa,CAAC;IAC7B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAChE,CAAC;AAED,SAAgB,QAAQ,CAAC,IAAgB,EAAE,IAAY;IACrD,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,MAAM,CAAC;AAC9D,CAAC;AAED,SAAgB,QAAQ,CAAC,IAAgB;IACvC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,UAAU,CACnG,CAAC;AACJ,CAAC;AAED,SAAgB,MAAM,CAAC,IAAgB;IACrC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,MAAM,CAAC,IAAgB;IACrC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,MAAM,CAAC,CAAC;AACnE,CAAC;AAED,SAAgB,kBAAkB,CAAC,IAAgB;IACjD,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACtC,MAAM,IAAI,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,IAAI,MAAM,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9D,CAAC;QACD,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,SAAgB,gBAAgB,CAAC,IAAgB;IAC/C,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACtD,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACtC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK;YAC1C,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW;SACnD,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,SAAS;AACT,0EAA0E;AAE1E,MAAa,MAAM;IACjB,SAAS,CAAyB;IAClC,UAAU,CAAa;IACvB,mBAAmB,CAA6B;IAEhD,YACE,SAAiC,EACjC,UAAsB,EACtB,sBAAkD,EAAE;QAEpD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;IACjD,CAAC;IAED,aAAa,CAAC,IAAY;QACxB,OAAO,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC;IAC3D,CAAC;IAED,KAAK,CACH,MAA8B,EAC9B,UAKI,EAAE;QAEN,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;QACxC,MAAM,YAAY,GAAkB,EAAE,CAAC;QAEvC,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACjD,IAAI,IAAI,IAAI,QAAQ;gBAAE,SAAS,CAAC,gBAAgB;YAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;YAC7C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,0BAAU,CAAC,QAAQ,CAAC;YACtD,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,CAAC,GAAG,IAAA,oBAAQ,EAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACjC,YAAY,CAAC,IAAI,CAAC;gBAChB,IAAI;gBACJ,MAAM,EAAE,CAAC;gBACT,KAAK,EAAE,GAAG;gBACV,QAAQ;gBACR,UAAU,EAAE,IAAI;gBAChB,cAAc,EAAE,CAAC,CAAC,cAAc,KAAK,KAAK;gBAC1C,UAAU,EAAE,EAAE;gBACd,UAAU,EAAE,SAAS;aACtB,CAAC,CAAC;QACL,CAAC;QAED,2BAA2B;QAC3B,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,0BAAU,CAAC,aAAa,CAAC;YAC3D,YAAY,CAAC,IAAI,CAAC;gBAChB,IAAI;gBACJ,MAAM,EAAE,kBAAM,CAAC,GAAG;gBAClB,KAAK,EAAE,QAAQ;gBACf,QAAQ;gBACR,UAAU,EAAE,IAAI;gBAChB,cAAc,EAAE,CAAC,CAAC,cAAc,KAAK,KAAK;gBAC1C,UAAU,EAAE,EAAE;gBACd,UAAU,EAAE,SAAS;gBACrB,OAAO,EAAE,MAAM;aAChB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,gBAAgB,CAAC,YAAY,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,CAAC;CACF;AAtED,wBAsEC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js API route adapter for margin.
|
|
3
|
+
*
|
|
4
|
+
* One-liner health tracking for any API route:
|
|
5
|
+
*
|
|
6
|
+
* // pages/api/users.ts (Pages Router)
|
|
7
|
+
* import { withMargin } from 'margin-ts/adapters/nextjs';
|
|
8
|
+
* export default withMargin(handler);
|
|
9
|
+
*
|
|
10
|
+
* // app/api/users/route.ts (App Router)
|
|
11
|
+
* import { withMarginApp } from 'margin-ts/adapters/nextjs';
|
|
12
|
+
* export const GET = withMarginApp(handler);
|
|
13
|
+
*
|
|
14
|
+
* // Health endpoint
|
|
15
|
+
* // pages/api/margin/health.ts
|
|
16
|
+
* import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
|
|
17
|
+
* export default marginHealthHandler();
|
|
18
|
+
*
|
|
19
|
+
* Tracks latency, error rate, and request count per route.
|
|
20
|
+
* Zero dependencies beyond margin-ts core.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
Confidence,
|
|
25
|
+
Health,
|
|
26
|
+
Thresholds,
|
|
27
|
+
createThresholds,
|
|
28
|
+
classify,
|
|
29
|
+
Observation,
|
|
30
|
+
createExpression,
|
|
31
|
+
expressionToString,
|
|
32
|
+
expressionToDict,
|
|
33
|
+
} from '../index.js';
|
|
34
|
+
|
|
35
|
+
// -----------------------------------------------------------------------
|
|
36
|
+
// Shared state — singleton across all wrapped routes
|
|
37
|
+
// -----------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface RouteStats {
|
|
40
|
+
totalRequests: number;
|
|
41
|
+
totalErrors: number;
|
|
42
|
+
latencies: number[];
|
|
43
|
+
maxWindow: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const _routes = new Map<string, RouteStats>();
|
|
47
|
+
const _MAX_WINDOW = 200;
|
|
48
|
+
|
|
49
|
+
function getOrCreateRoute(route: string): RouteStats {
|
|
50
|
+
if (!_routes.has(route)) {
|
|
51
|
+
_routes.set(route, {
|
|
52
|
+
totalRequests: 0,
|
|
53
|
+
totalErrors: 0,
|
|
54
|
+
latencies: [],
|
|
55
|
+
maxWindow: _MAX_WINDOW,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return _routes.get(route)!;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function record(route: string, latencyMs: number, isError: boolean): void {
|
|
62
|
+
const stats = getOrCreateRoute(route);
|
|
63
|
+
stats.totalRequests++;
|
|
64
|
+
if (isError) stats.totalErrors++;
|
|
65
|
+
stats.latencies.push(latencyMs);
|
|
66
|
+
if (stats.latencies.length > stats.maxWindow) stats.latencies.shift();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// -----------------------------------------------------------------------
|
|
70
|
+
// Thresholds
|
|
71
|
+
// -----------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export interface NextjsThresholds {
|
|
74
|
+
p50Latency: Thresholds;
|
|
75
|
+
p99Latency: Thresholds;
|
|
76
|
+
errorRate: Thresholds;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const DEFAULT_NEXTJS_THRESHOLDS: NextjsThresholds = {
|
|
80
|
+
p50Latency: createThresholds(100, 500, false),
|
|
81
|
+
p99Latency: createThresholds(500, 2000, false),
|
|
82
|
+
errorRate: createThresholds(0.01, 0.10, false),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// -----------------------------------------------------------------------
|
|
86
|
+
// Classification
|
|
87
|
+
// -----------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function percentile(sorted: number[], p: number): number {
|
|
90
|
+
if (sorted.length === 0) return 0;
|
|
91
|
+
const idx = Math.ceil(sorted.length * p) - 1;
|
|
92
|
+
return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function classifyRoute(route: string, stats: RouteStats, thresholds: NextjsThresholds) {
|
|
96
|
+
if (stats.latencies.length < 3) return null;
|
|
97
|
+
|
|
98
|
+
const sorted = [...stats.latencies].sort((a, b) => a - b);
|
|
99
|
+
const p50 = percentile(sorted, 0.5);
|
|
100
|
+
const p99 = percentile(sorted, 0.99);
|
|
101
|
+
const errorRate = stats.totalRequests > 0 ? stats.totalErrors / stats.totalRequests : 0;
|
|
102
|
+
const now = new Date();
|
|
103
|
+
|
|
104
|
+
const observations: Observation[] = [
|
|
105
|
+
{
|
|
106
|
+
name: `${route}:p50`, health: classify(p50, Confidence.HIGH, thresholds.p50Latency),
|
|
107
|
+
value: p50, baseline: thresholds.p50Latency.intact, confidence: Confidence.HIGH,
|
|
108
|
+
higherIsBetter: false, provenance: [], measuredAt: now,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: `${route}:p99`, health: classify(p99, Confidence.HIGH, thresholds.p99Latency),
|
|
112
|
+
value: p99, baseline: thresholds.p99Latency.intact, confidence: Confidence.HIGH,
|
|
113
|
+
higherIsBetter: false, provenance: [], measuredAt: now,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: `${route}:errors`, health: classify(errorRate, Confidence.HIGH, thresholds.errorRate),
|
|
117
|
+
value: errorRate, baseline: 0.001, confidence: Confidence.HIGH,
|
|
118
|
+
higherIsBetter: false, provenance: [], measuredAt: now,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
return createExpression(observations, [], route);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
// Pages Router wrapper
|
|
127
|
+
// -----------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export interface WithMarginOptions {
|
|
130
|
+
route?: string;
|
|
131
|
+
thresholds?: NextjsThresholds;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wrap a Pages Router API handler with margin health tracking.
|
|
136
|
+
*
|
|
137
|
+
* export default withMargin(handler);
|
|
138
|
+
* export default withMargin(handler, { route: '/api/users' });
|
|
139
|
+
*/
|
|
140
|
+
export function withMargin(
|
|
141
|
+
handler: (req: any, res: any) => any,
|
|
142
|
+
options: WithMarginOptions = {},
|
|
143
|
+
): (req: any, res: any) => any {
|
|
144
|
+
return async (req: any, res: any) => {
|
|
145
|
+
const route = options.route || req.url || '/api/unknown';
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await handler(req, res);
|
|
150
|
+
const latency = Date.now() - start;
|
|
151
|
+
const status = res.statusCode || 200;
|
|
152
|
+
record(route, latency, status >= 500);
|
|
153
|
+
return result;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const latency = Date.now() - start;
|
|
156
|
+
record(route, latency, true);
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// -----------------------------------------------------------------------
|
|
163
|
+
// App Router wrapper
|
|
164
|
+
// -----------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Wrap an App Router handler with margin health tracking.
|
|
168
|
+
*
|
|
169
|
+
* export const GET = withMarginApp(handler);
|
|
170
|
+
* export const POST = withMarginApp(handler, { route: '/api/users' });
|
|
171
|
+
*/
|
|
172
|
+
export function withMarginApp(
|
|
173
|
+
handler: (req: any) => any,
|
|
174
|
+
options: WithMarginOptions = {},
|
|
175
|
+
): (req: any) => any {
|
|
176
|
+
return async (req: any) => {
|
|
177
|
+
const route = options.route || new URL(req.url || '/', 'http://localhost').pathname;
|
|
178
|
+
const start = Date.now();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const response = await handler(req);
|
|
182
|
+
const latency = Date.now() - start;
|
|
183
|
+
const status = response?.status || 200;
|
|
184
|
+
record(route, latency, status >= 500);
|
|
185
|
+
return response;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const latency = Date.now() - start;
|
|
188
|
+
record(route, latency, true);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// -----------------------------------------------------------------------
|
|
195
|
+
// Health endpoint
|
|
196
|
+
// -----------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Pages Router: health endpoint handler.
|
|
200
|
+
*
|
|
201
|
+
* // pages/api/margin/health.ts
|
|
202
|
+
* import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
|
|
203
|
+
* export default marginHealthHandler();
|
|
204
|
+
*/
|
|
205
|
+
export function marginHealthHandler(thresholds?: NextjsThresholds): (req: any, res: any) => void {
|
|
206
|
+
const t = thresholds || DEFAULT_NEXTJS_THRESHOLDS;
|
|
207
|
+
|
|
208
|
+
return (_req: any, res: any) => {
|
|
209
|
+
const routeHealth: Record<string, any> = {};
|
|
210
|
+
let worstOverall = Health.INTACT;
|
|
211
|
+
const SEVERITY: Record<Health, number> = {
|
|
212
|
+
[Health.INTACT]: 0, [Health.RECOVERING]: 1, [Health.DEGRADED]: 2,
|
|
213
|
+
[Health.ABLATED]: 3, [Health.OOD]: 4,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
for (const [route, stats] of _routes) {
|
|
217
|
+
const expr = classifyRoute(route, stats, t);
|
|
218
|
+
if (expr) {
|
|
219
|
+
routeHealth[route] = {
|
|
220
|
+
expression: expressionToString(expr),
|
|
221
|
+
...expressionToDict(expr),
|
|
222
|
+
requests: stats.totalRequests,
|
|
223
|
+
errors: stats.totalErrors,
|
|
224
|
+
};
|
|
225
|
+
for (const obs of expr.observations) {
|
|
226
|
+
if (SEVERITY[obs.health] > SEVERITY[worstOverall]) {
|
|
227
|
+
worstOverall = obs.health;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
res.status(200).json({
|
|
234
|
+
status: worstOverall,
|
|
235
|
+
routes: routeHealth,
|
|
236
|
+
totalRoutes: _routes.size,
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* App Router: health endpoint handler.
|
|
243
|
+
*
|
|
244
|
+
* // app/api/margin/health/route.ts
|
|
245
|
+
* import { marginHealthAppHandler } from 'margin-ts/adapters/nextjs';
|
|
246
|
+
* export const GET = marginHealthAppHandler();
|
|
247
|
+
*/
|
|
248
|
+
export function marginHealthAppHandler(thresholds?: NextjsThresholds): (req: any) => any {
|
|
249
|
+
const t = thresholds || DEFAULT_NEXTJS_THRESHOLDS;
|
|
250
|
+
|
|
251
|
+
return (_req: any) => {
|
|
252
|
+
const routeHealth: Record<string, any> = {};
|
|
253
|
+
let worstOverall = Health.INTACT;
|
|
254
|
+
const SEVERITY: Record<Health, number> = {
|
|
255
|
+
[Health.INTACT]: 0, [Health.RECOVERING]: 1, [Health.DEGRADED]: 2,
|
|
256
|
+
[Health.ABLATED]: 3, [Health.OOD]: 4,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
for (const [route, stats] of _routes) {
|
|
260
|
+
const expr = classifyRoute(route, stats, t);
|
|
261
|
+
if (expr) {
|
|
262
|
+
routeHealth[route] = {
|
|
263
|
+
expression: expressionToString(expr),
|
|
264
|
+
...expressionToDict(expr),
|
|
265
|
+
requests: stats.totalRequests,
|
|
266
|
+
errors: stats.totalErrors,
|
|
267
|
+
};
|
|
268
|
+
for (const obs of expr.observations) {
|
|
269
|
+
if (SEVERITY[obs.health] > SEVERITY[worstOverall]) {
|
|
270
|
+
worstOverall = obs.health;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return new Response(JSON.stringify({
|
|
277
|
+
status: worstOverall,
|
|
278
|
+
routes: routeHealth,
|
|
279
|
+
totalRoutes: _routes.size,
|
|
280
|
+
}), {
|
|
281
|
+
status: 200,
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Clear all tracked routes (for testing). */
|
|
288
|
+
export function resetRoutes(): void {
|
|
289
|
+
_routes.clear();
|
|
290
|
+
}
|