@aforoai/metering 1.0.0 → 2.0.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/README.md +71 -25
- package/dist/cjs/client.d.ts +33 -35
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +193 -147
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/idempotency.d.ts +9 -9
- package/dist/cjs/idempotency.js +9 -9
- package/dist/cjs/index.d.ts +8 -8
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +20 -19
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/limits.d.ts +47 -7
- package/dist/cjs/limits.d.ts.map +1 -1
- package/dist/cjs/limits.js +65 -10
- package/dist/cjs/limits.js.map +1 -1
- package/dist/cjs/middleware/common.d.ts +24 -2
- package/dist/cjs/middleware/common.d.ts.map +1 -1
- package/dist/cjs/middleware/common.js +66 -4
- package/dist/cjs/middleware/common.js.map +1 -1
- package/dist/cjs/middleware/express.d.ts +11 -6
- package/dist/cjs/middleware/express.d.ts.map +1 -1
- package/dist/cjs/middleware/express.js +25 -62
- package/dist/cjs/middleware/express.js.map +1 -1
- package/dist/cjs/middleware/fastify.d.ts +9 -5
- package/dist/cjs/middleware/fastify.d.ts.map +1 -1
- package/dist/cjs/middleware/fastify.js +28 -48
- package/dist/cjs/middleware/fastify.js.map +1 -1
- package/dist/cjs/middleware/koa.d.ts +11 -6
- package/dist/cjs/middleware/koa.d.ts.map +1 -1
- package/dist/cjs/middleware/koa.js +28 -46
- package/dist/cjs/middleware/koa.js.map +1 -1
- package/dist/cjs/transport-core/core.d.ts +355 -0
- package/dist/cjs/transport-core/core.d.ts.map +1 -0
- package/dist/cjs/transport-core/core.js +1047 -0
- package/dist/cjs/transport-core/core.js.map +1 -0
- package/dist/cjs/types.d.ts +108 -20
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/client.js +191 -145
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/idempotency.js +9 -9
- package/dist/esm/index.js +7 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/limits.js +62 -10
- package/dist/esm/limits.js.map +1 -1
- package/dist/esm/middleware/common.js +62 -4
- package/dist/esm/middleware/common.js.map +1 -1
- package/dist/esm/middleware/express.js +24 -61
- package/dist/esm/middleware/express.js.map +1 -1
- package/dist/esm/middleware/fastify.js +27 -47
- package/dist/esm/middleware/fastify.js.map +1 -1
- package/dist/esm/middleware/koa.js +27 -45
- package/dist/esm/middleware/koa.js.map +1 -1
- package/dist/esm/package.json +1 -0
- package/dist/esm/transport-core/core.js +1038 -0
- package/dist/esm/transport-core/core.js.map +1 -0
- package/dist/types/client.d.ts +33 -35
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/idempotency.d.ts +9 -9
- package/dist/types/index.d.ts +8 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/limits.d.ts +47 -7
- package/dist/types/limits.d.ts.map +1 -1
- package/dist/types/middleware/common.d.ts +24 -2
- package/dist/types/middleware/common.d.ts.map +1 -1
- package/dist/types/middleware/express.d.ts +11 -6
- package/dist/types/middleware/express.d.ts.map +1 -1
- package/dist/types/middleware/fastify.d.ts +9 -5
- package/dist/types/middleware/fastify.d.ts.map +1 -1
- package/dist/types/middleware/koa.d.ts +11 -6
- package/dist/types/middleware/koa.d.ts.map +1 -1
- package/dist/types/transport-core/core.d.ts +355 -0
- package/dist/types/transport-core/core.d.ts.map +1 -0
- package/dist/types/types.d.ts +108 -20
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +22 -6
- package/dist/cjs/buffer.d.ts +0 -27
- package/dist/cjs/buffer.d.ts.map +0 -1
- package/dist/cjs/buffer.js +0 -75
- package/dist/cjs/buffer.js.map +0 -1
- package/dist/cjs/transport.d.ts +0 -39
- package/dist/cjs/transport.d.ts.map +0 -1
- package/dist/cjs/transport.js +0 -116
- package/dist/cjs/transport.js.map +0 -1
- package/dist/esm/buffer.js +0 -71
- package/dist/esm/buffer.js.map +0 -1
- package/dist/esm/transport.js +0 -112
- package/dist/esm/transport.js.map +0 -1
- package/dist/types/buffer.d.ts +0 -27
- package/dist/types/buffer.d.ts.map +0 -1
- package/dist/types/transport.d.ts +0 -39
- package/dist/types/transport.d.ts.map +0 -1
package/dist/esm/limits.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* billed. The server reports it per event (`errors[]` in the batch response,
|
|
6
6
|
* indexed), so the rest of the batch survives — but the SDK sends in the
|
|
7
7
|
* background, so that report reaches nobody: the usage is simply gone. Checking
|
|
8
|
-
* the same limits at `track()`
|
|
9
|
-
*
|
|
8
|
+
* the same limits at `track()` reports the problem where it happens: the event
|
|
9
|
+
* is not buffered or sent, it is counted in `droppedCount`, WARN-logged with
|
|
10
|
+
* the field, the limit and the offending value, and handed to the opt-in
|
|
11
|
+
* `onDrop` hook with reason `'invalid'`. `track()` does not throw for it.
|
|
10
12
|
*
|
|
11
13
|
* Source: `dto/IngestUsageEventRequest` (aforo-nextgen-usage-ingestor-service) —
|
|
12
14
|
* the `@Size` and `@Digits` bean constraints, which are compiled into the
|
|
@@ -21,9 +23,15 @@
|
|
|
21
23
|
* Only `occurredAt`'s shape is checked, since a malformed timestamp is invalid
|
|
22
24
|
* under every configuration.
|
|
23
25
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* offending value.
|
|
26
|
+
* The limit check never truncates or rounds a value the caller set — that would
|
|
27
|
+
* change what is billed. The offending event is dropped instead (reason
|
|
28
|
+
* `'invalid'`), naming the field, the limit, and the offending value.
|
|
29
|
+
*
|
|
30
|
+
* The one exception is a label the SDK itself reads off an incoming request
|
|
31
|
+
* (the middlewares' `endpointPath` and `httpMethod`): dropping there would let
|
|
32
|
+
* an API consumer avoid metering by sending an over-long path. Those are cut to
|
|
33
|
+
* the limit where they are derived (`truncateRequestLabel`) and the event is
|
|
34
|
+
* still sent.
|
|
27
35
|
*/
|
|
28
36
|
/** Maximum characters the ingestor accepts per string field (`@Size(max = ...)`). */
|
|
29
37
|
export const MAX_LENGTHS = {
|
|
@@ -36,8 +44,16 @@ export const MAX_LENGTHS = {
|
|
|
36
44
|
sessionId: 64,
|
|
37
45
|
agentId: 36,
|
|
38
46
|
toolName: 64,
|
|
47
|
+
capabilityName: 64,
|
|
39
48
|
endpointPath: 512,
|
|
40
49
|
httpMethod: 16,
|
|
50
|
+
subscriptionId: 64,
|
|
51
|
+
grpcService: 255,
|
|
52
|
+
grpcMethod: 128,
|
|
53
|
+
gqlOperationName: 255,
|
|
54
|
+
wsConnectionId: 64,
|
|
55
|
+
mqttTopic: 500,
|
|
56
|
+
mqttClientId: 128,
|
|
41
57
|
};
|
|
42
58
|
/** `@Digits(integer = 14, fraction = 6)` — `usage_events.quantity` is NUMERIC(20,6). */
|
|
43
59
|
export const MAX_QUANTITY_INTEGER_DIGITS = 14;
|
|
@@ -56,6 +72,42 @@ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?([Z
|
|
|
56
72
|
function lengthOf(value) {
|
|
57
73
|
return value.length;
|
|
58
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Cut `value` to at most `max` UTF-16 code units — how the ingestor counts —
|
|
77
|
+
* without leaving half a surrogate pair at the end.
|
|
78
|
+
*/
|
|
79
|
+
export function truncateToLimit(value, max) {
|
|
80
|
+
if (value.length <= max)
|
|
81
|
+
return value;
|
|
82
|
+
let end = Math.max(0, max);
|
|
83
|
+
const last = end > 0 ? value.charCodeAt(end - 1) : 0;
|
|
84
|
+
// A high surrogate at the cut means its low half was cut off: drop it too.
|
|
85
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
86
|
+
end -= 1;
|
|
87
|
+
return value.slice(0, end);
|
|
88
|
+
}
|
|
89
|
+
/** Labels already reported as truncated — one WARN per label name per process. */
|
|
90
|
+
const truncationWarned = new Set();
|
|
91
|
+
/**
|
|
92
|
+
* Bound a label the SDK derived from an incoming request to the ingestor's
|
|
93
|
+
* limit for `field`. Logs one WARN per field per process. Not for values the
|
|
94
|
+
* SDK caller sets: those are never altered (an over-long one drops the event).
|
|
95
|
+
*/
|
|
96
|
+
export function truncateRequestLabel(field, value) {
|
|
97
|
+
const max = MAX_LENGTHS[field];
|
|
98
|
+
if (typeof value !== 'string' || value.length <= max)
|
|
99
|
+
return value;
|
|
100
|
+
if (!truncationWarned.has(field)) {
|
|
101
|
+
truncationWarned.add(field);
|
|
102
|
+
console.warn(`[aforo] ${field} taken from the request was longer than the ingestor's limit and was `
|
|
103
|
+
+ `truncated to ${max} characters; the event is still sent. Logged once per label.`);
|
|
104
|
+
}
|
|
105
|
+
return truncateToLimit(value, max);
|
|
106
|
+
}
|
|
107
|
+
/** Test hook: forget which labels were already reported as truncated. */
|
|
108
|
+
export function resetTruncationWarnings() {
|
|
109
|
+
truncationWarned.clear();
|
|
110
|
+
}
|
|
59
111
|
function lengthError(field, value) {
|
|
60
112
|
if (value === undefined || value === null)
|
|
61
113
|
return null;
|
|
@@ -65,7 +117,7 @@ function lengthError(field, value) {
|
|
|
65
117
|
if (len <= max)
|
|
66
118
|
return null;
|
|
67
119
|
return `${field} is ${len} characters, exceeding the ingestor's ${max}-character limit `
|
|
68
|
-
+ `(value starts "${text.slice(0,
|
|
120
|
+
+ `(value starts "${text.slice(0, 80)}"). Shorten it — the SDK will not truncate it, `
|
|
69
121
|
+ 'because a truncated id bills the wrong thing.';
|
|
70
122
|
}
|
|
71
123
|
/**
|
|
@@ -107,18 +159,18 @@ function quantityError(quantity) {
|
|
|
107
159
|
}
|
|
108
160
|
function occurredAtError(occurredAt) {
|
|
109
161
|
if (!ISO_DATE_TIME.test(occurredAt)) {
|
|
110
|
-
return `occurredAt "${occurredAt}" is not an ISO-8601 timestamp `
|
|
162
|
+
return `occurredAt "${String(occurredAt).slice(0, 80)}" is not an ISO-8601 timestamp `
|
|
111
163
|
+ '(expected e.g. "2026-03-01T14:30:00Z").';
|
|
112
164
|
}
|
|
113
165
|
if (Number.isNaN(Date.parse(occurredAt))) {
|
|
114
|
-
return `occurredAt "${occurredAt}" is not a valid date.`;
|
|
166
|
+
return `occurredAt "${String(occurredAt).slice(0, 80)}" is not a valid date.`;
|
|
115
167
|
}
|
|
116
168
|
return null;
|
|
117
169
|
}
|
|
118
170
|
/**
|
|
119
171
|
* Describe the first ingestor constraint this event breaks, or `null` when it
|
|
120
|
-
* would be accepted.
|
|
121
|
-
*
|
|
172
|
+
* would be accepted. `AforoClient.track()` uses it to drop an invalid event
|
|
173
|
+
* (reason `'invalid'`) before it is buffered.
|
|
122
174
|
*/
|
|
123
175
|
export function describeLimitViolation(event) {
|
|
124
176
|
if (event.idempotencyKey !== undefined && event.idempotencyKey !== null
|
package/dist/esm/limits.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { truncateRequestLabel } from '../limits.js';
|
|
2
|
+
import { AforoClient } from '../client.js';
|
|
3
|
+
import { normalizePath as heuristicNormalize } from '../path-normalizer.js';
|
|
1
4
|
/**
|
|
2
5
|
* Metric recorded for each request when `metricName` is not configured.
|
|
3
6
|
*
|
|
@@ -21,11 +24,17 @@ export function resolveMetricName(options, req, res) {
|
|
|
21
24
|
}
|
|
22
25
|
return options.metricName || DEFAULT_METRIC_NAME;
|
|
23
26
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Request path without its query string. Read off the incoming request, so an
|
|
29
|
+
* over-long path is cut to the ingestor's limit (512) instead of dropping the
|
|
30
|
+
* event — otherwise a caller could avoid metering with a long URL.
|
|
31
|
+
*/
|
|
27
32
|
export function endpointPathOf(url) {
|
|
28
|
-
return (url.split('?')[0] || '/')
|
|
33
|
+
return truncateRequestLabel('endpointPath', String(url).split('?')[0] || '/');
|
|
34
|
+
}
|
|
35
|
+
/** HTTP method off the incoming request, cut to the ingestor's limit (16). */
|
|
36
|
+
export function httpMethodOf(method) {
|
|
37
|
+
return truncateRequestLabel('httpMethod', String(method));
|
|
29
38
|
}
|
|
30
39
|
/** First non-empty string among the candidates, or null. */
|
|
31
40
|
export function firstNonEmpty(...values) {
|
|
@@ -38,4 +47,53 @@ export function firstNonEmpty(...values) {
|
|
|
38
47
|
}
|
|
39
48
|
return null;
|
|
40
49
|
}
|
|
50
|
+
/** The client a middleware meters through: the app's own, or one built from the options. */
|
|
51
|
+
export function middlewareClient(options) {
|
|
52
|
+
if (options?.client)
|
|
53
|
+
return options.client;
|
|
54
|
+
if (!options?.apiKey) {
|
|
55
|
+
throw new Error('Aforo middleware needs either `client` (an AforoClient your app owns) or `apiKey`.');
|
|
56
|
+
}
|
|
57
|
+
return new AforoClient({ ...(options.clientOptions ?? {}), apiKey: options.apiKey, baseUrl: options.baseUrl });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the per-request customer lookup. Throws at setup when neither a
|
|
61
|
+
* resolver nor the explicit `trustCustomerHeader` opt-in is configured: the
|
|
62
|
+
* X-Customer-Id header is set by whoever calls the API, so it is never read
|
|
63
|
+
* by default.
|
|
64
|
+
*/
|
|
65
|
+
export function customerLookup(options, readHeader) {
|
|
66
|
+
const legacy = options?.customerId;
|
|
67
|
+
const resolver = options?.customerResolver ??
|
|
68
|
+
(typeof legacy === 'function' ? legacy : typeof legacy === 'string' && legacy.trim() ? () => legacy : undefined);
|
|
69
|
+
const trustHeader = options?.trustCustomerHeader === true;
|
|
70
|
+
if (!resolver && !trustHeader) {
|
|
71
|
+
throw new Error('Aforo middleware needs a customerResolver: (req) => the Aforo customer id from identity your app ' +
|
|
72
|
+
'authenticated (session, verified JWT subject, API-key lookup). To bill the X-Customer-Id header ' +
|
|
73
|
+
'instead — only safe behind a proxy that sets it — pass trustCustomerHeader: true.');
|
|
74
|
+
}
|
|
75
|
+
return (req, headerSource = req) => {
|
|
76
|
+
const fromResolver = resolver ? firstNonEmpty(resolver(req)) : null;
|
|
77
|
+
if (fromResolver)
|
|
78
|
+
return fromResolver;
|
|
79
|
+
return trustHeader ? firstNonEmpty(readHeader(headerSource)) : null;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The endpointPath to send: the query string removed, dynamic segments
|
|
84
|
+
* collapsed unless `normalizePath` is false (a framework route template wins),
|
|
85
|
+
* and cut to the ingestor's 512-character limit.
|
|
86
|
+
*/
|
|
87
|
+
export function endpointPathFor(options, rawPath, req, routeTemplate) {
|
|
88
|
+
const path = String(rawPath ?? '/').split('?')[0] || '/';
|
|
89
|
+
const mode = options?.normalizePath ?? true;
|
|
90
|
+
let out = path;
|
|
91
|
+
if (typeof mode === 'function') {
|
|
92
|
+
out = String(mode(path, req) || path);
|
|
93
|
+
}
|
|
94
|
+
else if (mode) {
|
|
95
|
+
out = heuristicNormalize(path, typeof routeTemplate === 'string' && routeTemplate ? routeTemplate : undefined);
|
|
96
|
+
}
|
|
97
|
+
return endpointPathOf(out);
|
|
98
|
+
}
|
|
41
99
|
//# sourceMappingURL=common.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,99 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
|
|
1
|
+
import { isPreflight, resolveMetricName, httpMethodOf, middlewareClient, customerLookup, endpointPathFor } from './common.js';
|
|
3
2
|
const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
|
|
4
3
|
/**
|
|
5
|
-
* Express middleware that
|
|
4
|
+
* Express middleware that records one usage event per request.
|
|
6
5
|
*
|
|
7
|
-
* Hooks into `res.on('finish')` — runs AFTER the response is sent
|
|
8
|
-
*
|
|
6
|
+
* Hooks into `res.on('finish')` — runs AFTER the response is sent, so it adds
|
|
7
|
+
* no latency to the request. Metering never throws into the request.
|
|
9
8
|
*
|
|
10
9
|
* ```typescript
|
|
10
|
+
* import { AforoClient } from '@aforoai/metering';
|
|
11
11
|
* import { expressMiddleware } from '@aforoai/metering/middleware/express';
|
|
12
|
-
*
|
|
12
|
+
* const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
|
|
13
|
+
* app.use(expressMiddleware({ client: aforo, customerResolver: (req) => req.user?.aforoCustomerId }));
|
|
14
|
+
* process.once('SIGTERM', async () => { await aforo.shutdown(); process.exit(0); });
|
|
13
15
|
* ```
|
|
14
16
|
*/
|
|
15
17
|
export function expressMiddleware(options) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
baseUrl: options.baseUrl,
|
|
19
|
-
...options.clientOptions,
|
|
20
|
-
});
|
|
18
|
+
const lookupCustomer = customerLookup(options, (req) => req.headers?.['x-customer-id']);
|
|
19
|
+
const client = middlewareClient(options);
|
|
21
20
|
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
22
21
|
const excludeStatusCodes = options.excludeStatusCodes ?? [];
|
|
23
|
-
|
|
24
|
-
// Capture timing
|
|
22
|
+
const handler = function aforoMeteringMiddleware(req, res, next) {
|
|
25
23
|
const startTime = Date.now();
|
|
26
24
|
res.on('finish', () => {
|
|
27
25
|
try {
|
|
28
26
|
const path = req.originalUrl || req.url || '/';
|
|
29
27
|
const method = req.method || 'UNKNOWN';
|
|
30
28
|
const statusCode = res.statusCode || 0;
|
|
31
|
-
// Check exclusions
|
|
32
29
|
if (isPreflight(method))
|
|
33
30
|
return;
|
|
34
31
|
if (excludePaths.some((p) => path.startsWith(p)))
|
|
35
32
|
return;
|
|
36
33
|
if (excludeStatusCodes.includes(statusCode))
|
|
37
34
|
return;
|
|
38
|
-
|
|
39
|
-
const metricName = resolveMetricName(options, req, res);
|
|
40
|
-
// Resolve quantity
|
|
41
|
-
let quantity;
|
|
42
|
-
if (typeof options.quantity === 'function') {
|
|
43
|
-
quantity = options.quantity(req, res);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
quantity = options.quantity ?? 1;
|
|
47
|
-
}
|
|
48
|
-
// Resolve customer ID
|
|
49
|
-
let customerId;
|
|
50
|
-
if (typeof options.customerId === 'function') {
|
|
51
|
-
customerId = options.customerId(req);
|
|
52
|
-
}
|
|
53
|
-
else if (options.customerId) {
|
|
54
|
-
customerId = options.customerId;
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
customerId = extractCustomerId(req);
|
|
58
|
-
}
|
|
59
|
-
if (!customerId)
|
|
60
|
-
return; // Can't meter without a customer
|
|
35
|
+
const quantity = typeof options.quantity === 'function' ? options.quantity(req, res) : options.quantity ?? 1;
|
|
61
36
|
if (!(quantity > 0))
|
|
62
37
|
return; // The ingestor rejects quantity <= 0
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (options.metadata) {
|
|
66
|
-
metadata = options.metadata(req, res);
|
|
67
|
-
}
|
|
38
|
+
const route = typeof req.route?.path === 'string' ? `${req.baseUrl ?? ''}${req.route.path}` : undefined;
|
|
39
|
+
// No customer → track() drops it as 'invalid' and counts it; the request is unaffected.
|
|
68
40
|
client.track({
|
|
69
|
-
customerId,
|
|
70
|
-
metricName,
|
|
41
|
+
customerId: lookupCustomer(req) ?? '',
|
|
42
|
+
metricName: resolveMetricName(options, req, res),
|
|
71
43
|
quantity,
|
|
72
|
-
metadata,
|
|
44
|
+
metadata: options.metadata ? options.metadata(req, res) : undefined,
|
|
73
45
|
productType: options.productType,
|
|
74
|
-
endpointPath:
|
|
75
|
-
httpMethod: method,
|
|
46
|
+
endpointPath: endpointPathFor(options, path, req, route),
|
|
47
|
+
httpMethod: httpMethodOf(method),
|
|
76
48
|
statusCode,
|
|
77
49
|
responseTimeMs: Date.now() - startTime,
|
|
78
|
-
}).catch(() => { });
|
|
50
|
+
}).catch((err) => console.debug(`[aforo] metering skipped: ${err?.message ?? err}`));
|
|
79
51
|
}
|
|
80
|
-
catch {
|
|
81
|
-
// Never let metering errors affect the API
|
|
52
|
+
catch (err) {
|
|
53
|
+
// Never let metering errors affect the API.
|
|
54
|
+
console.debug(`[aforo] metering skipped: ${err?.message ?? err}`);
|
|
82
55
|
}
|
|
83
56
|
});
|
|
84
57
|
next();
|
|
85
58
|
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Default customer resolution: authenticated user id, then X-Customer-Id.
|
|
89
|
-
*
|
|
90
|
-
* The caller's X-Api-Key header is deliberately NOT used: that is the end
|
|
91
|
-
* user's secret, and using it as customerId wrote credentials into billing data
|
|
92
|
-
* while never matching an Aforo customer. Configure `customerId` to map your
|
|
93
|
-
* callers to Aforo customer ids.
|
|
94
|
-
*/
|
|
95
|
-
function extractCustomerId(req) {
|
|
96
|
-
return firstNonEmpty(req.user?.id, req.user?.sub, req.headers?.['x-customer-id']);
|
|
59
|
+
return Object.assign(handler, { client });
|
|
97
60
|
}
|
|
98
61
|
// Also export as `middleware` for convenience
|
|
99
62
|
export const middleware = expressMiddleware;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/middleware/express.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/middleware/express.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
|
|
1
|
+
import { isPreflight, resolveMetricName, httpMethodOf, middlewareClient, customerLookup, endpointPathFor } from './common.js';
|
|
3
2
|
const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
|
|
4
3
|
/**
|
|
5
|
-
* Fastify plugin that
|
|
6
|
-
*
|
|
7
|
-
* Uses the `onResponse` hook — runs AFTER the response is sent.
|
|
4
|
+
* Fastify plugin that records one usage event per request in the
|
|
5
|
+
* `onResponse` hook (after the response is sent).
|
|
8
6
|
*
|
|
9
7
|
* ```typescript
|
|
8
|
+
* import { AforoClient } from '@aforoai/metering';
|
|
10
9
|
* import { fastifyPlugin } from '@aforoai/metering/middleware/fastify';
|
|
11
|
-
*
|
|
10
|
+
* const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
|
|
11
|
+
* fastify.register(fastifyPlugin, { client: aforo, customerResolver: (request) => request.user?.aforoCustomerId });
|
|
12
12
|
* ```
|
|
13
|
+
*
|
|
14
|
+
* When the plugin creates its own client (apiKey instead of client), it shuts
|
|
15
|
+
* that client down in Fastify's onClose hook. A client you pass in is yours to shut down.
|
|
13
16
|
*/
|
|
14
17
|
export async function fastifyPlugin(fastify, options) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...options.clientOptions,
|
|
19
|
-
});
|
|
18
|
+
const lookupCustomer = customerLookup(options, (request) => request.headers?.['x-customer-id']);
|
|
19
|
+
const owned = !options.client;
|
|
20
|
+
const client = middlewareClient(options);
|
|
20
21
|
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
21
22
|
const excludeStatusCodes = options.excludeStatusCodes ?? [];
|
|
22
23
|
fastify.addHook('onResponse', (request, reply, done) => {
|
|
@@ -30,56 +31,35 @@ export async function fastifyPlugin(fastify, options) {
|
|
|
30
31
|
return done();
|
|
31
32
|
if (excludeStatusCodes.includes(statusCode))
|
|
32
33
|
return done();
|
|
33
|
-
const
|
|
34
|
-
let quantity;
|
|
35
|
-
if (typeof options.quantity === 'function') {
|
|
36
|
-
quantity = options.quantity(request, reply);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
quantity = options.quantity ?? 1;
|
|
40
|
-
}
|
|
41
|
-
let customerId;
|
|
42
|
-
if (typeof options.customerId === 'function') {
|
|
43
|
-
customerId = options.customerId(request);
|
|
44
|
-
}
|
|
45
|
-
else if (options.customerId) {
|
|
46
|
-
customerId = options.customerId;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// Never the caller's X-Api-Key: that is a secret, not a customer id.
|
|
50
|
-
customerId = firstNonEmpty(request.user?.id, request.headers?.['x-customer-id']);
|
|
51
|
-
}
|
|
52
|
-
if (!customerId)
|
|
53
|
-
return done();
|
|
34
|
+
const quantity = typeof options.quantity === 'function' ? options.quantity(request, reply) : options.quantity ?? 1;
|
|
54
35
|
if (!(quantity > 0))
|
|
55
36
|
return done(); // The ingestor rejects quantity <= 0
|
|
56
|
-
let metadata;
|
|
57
|
-
if (options.metadata) {
|
|
58
|
-
metadata = options.metadata(request, reply);
|
|
59
|
-
}
|
|
60
37
|
const elapsed = typeof reply.elapsedTime === 'number'
|
|
61
38
|
? reply.elapsedTime
|
|
62
39
|
: typeof reply.getResponseTime === 'function' ? reply.getResponseTime() : undefined;
|
|
63
40
|
client.track({
|
|
64
|
-
customerId,
|
|
65
|
-
metricName,
|
|
41
|
+
customerId: lookupCustomer(request) ?? '',
|
|
42
|
+
metricName: resolveMetricName(options, request, reply),
|
|
66
43
|
quantity,
|
|
67
|
-
metadata,
|
|
44
|
+
metadata: options.metadata ? options.metadata(request, reply) : undefined,
|
|
68
45
|
productType: options.productType,
|
|
69
|
-
endpointPath:
|
|
70
|
-
httpMethod: method,
|
|
46
|
+
endpointPath: endpointPathFor(options, path, request, request.routeOptions?.url ?? request.routerPath),
|
|
47
|
+
httpMethod: httpMethodOf(method),
|
|
71
48
|
statusCode,
|
|
72
49
|
...(typeof elapsed === 'number' ? { responseTimeMs: Math.round(elapsed) } : {}),
|
|
73
|
-
}).catch(() => { });
|
|
50
|
+
}).catch((err) => console.debug(`[aforo] metering skipped: ${err?.message ?? err}`));
|
|
74
51
|
}
|
|
75
|
-
catch {
|
|
76
|
-
// Never let metering affect the API
|
|
52
|
+
catch (err) {
|
|
53
|
+
// Never let metering affect the API.
|
|
54
|
+
console.debug(`[aforo] metering skipped: ${err?.message ?? err}`);
|
|
77
55
|
}
|
|
78
56
|
done();
|
|
79
57
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
58
|
+
if (owned) {
|
|
59
|
+
fastify.addHook('onClose', async () => {
|
|
60
|
+
await client.shutdown();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
83
63
|
}
|
|
84
64
|
// Mark as fastify plugin
|
|
85
65
|
fastifyPlugin[Symbol.for('skip-override')] = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
|
|
1
|
+
import { isPreflight, resolveMetricName, httpMethodOf, middlewareClient, customerLookup, endpointPathFor } from './common.js';
|
|
3
2
|
const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
|
|
4
3
|
/**
|
|
5
|
-
* Koa middleware that
|
|
6
|
-
*
|
|
7
|
-
* Captures after `await next()` completes — runs after the response is generated.
|
|
4
|
+
* Koa middleware that records one usage event per request, after `await next()`.
|
|
5
|
+
* Metering never throws into the request.
|
|
8
6
|
*
|
|
9
7
|
* ```typescript
|
|
8
|
+
* import { AforoClient } from '@aforoai/metering';
|
|
10
9
|
* import { koaMiddleware } from '@aforoai/metering/middleware/koa';
|
|
11
|
-
*
|
|
10
|
+
* const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
|
|
11
|
+
* app.use(koaMiddleware({ client: aforo, customerResolver: (ctx) => ctx.state.user?.aforoCustomerId }));
|
|
12
12
|
* ```
|
|
13
|
+
*
|
|
14
|
+
* `customerResolver` receives the Koa context.
|
|
13
15
|
*/
|
|
14
16
|
export function koaMiddleware(options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...options
|
|
19
|
-
|
|
17
|
+
// customerResolver receives the Koa context; the deprecated customerId function kept receiving ctx.request.
|
|
18
|
+
const legacy = options.customerId;
|
|
19
|
+
const lookupCustomer = customerLookup({
|
|
20
|
+
...options,
|
|
21
|
+
customerResolver: options.customerResolver ?? (typeof legacy === 'function' ? (ctx) => legacy(ctx?.request) : undefined),
|
|
22
|
+
}, (ctx) => ctx?.get?.('x-customer-id') || ctx?.request?.headers?.['x-customer-id']);
|
|
23
|
+
const client = middlewareClient(options);
|
|
20
24
|
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
21
25
|
const excludeStatusCodes = options.excludeStatusCodes ?? [];
|
|
22
|
-
|
|
26
|
+
const handler = async function aforoMeteringMiddleware(ctx, next) {
|
|
23
27
|
const startTime = Date.now();
|
|
24
28
|
await next();
|
|
25
29
|
try {
|
|
@@ -32,48 +36,26 @@ export function koaMiddleware(options) {
|
|
|
32
36
|
return;
|
|
33
37
|
if (excludeStatusCodes.includes(statusCode))
|
|
34
38
|
return;
|
|
35
|
-
const
|
|
36
|
-
let quantity;
|
|
37
|
-
if (typeof options.quantity === 'function') {
|
|
38
|
-
quantity = options.quantity(ctx.request, ctx.response);
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
quantity = options.quantity ?? 1;
|
|
42
|
-
}
|
|
43
|
-
let customerId;
|
|
44
|
-
if (typeof options.customerId === 'function') {
|
|
45
|
-
customerId = options.customerId(ctx.request);
|
|
46
|
-
}
|
|
47
|
-
else if (options.customerId) {
|
|
48
|
-
customerId = options.customerId;
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
// Never the caller's X-Api-Key: that is a secret, not a customer id.
|
|
52
|
-
customerId = firstNonEmpty(ctx.state?.user?.id, ctx.get('x-customer-id'));
|
|
53
|
-
}
|
|
54
|
-
if (!customerId)
|
|
55
|
-
return;
|
|
39
|
+
const quantity = typeof options.quantity === 'function' ? options.quantity(ctx.request, ctx.response) : options.quantity ?? 1;
|
|
56
40
|
if (!(quantity > 0))
|
|
57
41
|
return; // The ingestor rejects quantity <= 0
|
|
58
|
-
let metadata;
|
|
59
|
-
if (options.metadata) {
|
|
60
|
-
metadata = options.metadata(ctx.request, ctx.response);
|
|
61
|
-
}
|
|
62
42
|
client.track({
|
|
63
|
-
customerId,
|
|
64
|
-
metricName,
|
|
43
|
+
customerId: lookupCustomer(ctx) ?? '',
|
|
44
|
+
metricName: resolveMetricName(options, ctx.request, ctx.response),
|
|
65
45
|
quantity,
|
|
66
|
-
metadata,
|
|
46
|
+
metadata: options.metadata ? options.metadata(ctx.request, ctx.response) : undefined,
|
|
67
47
|
productType: options.productType,
|
|
68
|
-
endpointPath:
|
|
69
|
-
httpMethod: method,
|
|
48
|
+
endpointPath: endpointPathFor(options, path, ctx, ctx._matchedRoute ?? ctx.routerPath),
|
|
49
|
+
httpMethod: httpMethodOf(method),
|
|
70
50
|
statusCode,
|
|
71
51
|
responseTimeMs: Date.now() - startTime,
|
|
72
|
-
}).catch(() => { });
|
|
52
|
+
}).catch((err) => console.debug(`[aforo] metering skipped: ${err?.message ?? err}`));
|
|
73
53
|
}
|
|
74
|
-
catch {
|
|
75
|
-
// Never let metering affect the API
|
|
54
|
+
catch (err) {
|
|
55
|
+
// Never let metering affect the API.
|
|
56
|
+
console.debug(`[aforo] metering skipped: ${err?.message ?? err}`);
|
|
76
57
|
}
|
|
77
58
|
};
|
|
59
|
+
return Object.assign(handler, { client });
|
|
78
60
|
}
|
|
79
61
|
//# sourceMappingURL=koa.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"koa.js","sourceRoot":"","sources":["../../../src/middleware/koa.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"koa.js","sourceRoot":"","sources":["../../../src/middleware/koa.ts"],"names":[],"mappings":"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"}
|