@aforoai/metering 1.1.2 → 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 +46 -28
- package/dist/cjs/client.d.ts +32 -50
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +104 -215
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/middleware/common.d.ts +16 -0
- package/dist/cjs/middleware/common.d.ts.map +1 -1
- package/dist/cjs/middleware/common.js +54 -0
- package/dist/cjs/middleware/common.js.map +1 -1
- package/dist/cjs/middleware/express.d.ts +10 -5
- package/dist/cjs/middleware/express.d.ts.map +1 -1
- package/dist/cjs/middleware/express.js +22 -59
- package/dist/cjs/middleware/express.js.map +1 -1
- package/dist/cjs/middleware/fastify.d.ts +8 -4
- package/dist/cjs/middleware/fastify.d.ts.map +1 -1
- package/dist/cjs/middleware/fastify.js +25 -45
- package/dist/cjs/middleware/fastify.js.map +1 -1
- package/dist/cjs/middleware/koa.d.ts +10 -5
- package/dist/cjs/middleware/koa.d.ts.map +1 -1
- package/dist/cjs/middleware/koa.js +25 -43
- 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 +82 -46
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/client.js +103 -214
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/middleware/common.js +51 -0
- package/dist/esm/middleware/common.js.map +1 -1
- package/dist/esm/middleware/express.js +23 -60
- package/dist/esm/middleware/express.js.map +1 -1
- package/dist/esm/middleware/fastify.js +26 -46
- package/dist/esm/middleware/fastify.js.map +1 -1
- package/dist/esm/middleware/koa.js +26 -44
- package/dist/esm/middleware/koa.js.map +1 -1
- 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 +32 -50
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/middleware/common.d.ts +16 -0
- package/dist/types/middleware/common.d.ts.map +1 -1
- package/dist/types/middleware/express.d.ts +10 -5
- package/dist/types/middleware/express.d.ts.map +1 -1
- package/dist/types/middleware/fastify.d.ts +8 -4
- package/dist/types/middleware/fastify.d.ts.map +1 -1
- package/dist/types/middleware/koa.d.ts +10 -5
- 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 +82 -46
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/cjs/buffer.d.ts +0 -33
- package/dist/cjs/buffer.d.ts.map +0 -1
- package/dist/cjs/buffer.js +0 -83
- package/dist/cjs/buffer.js.map +0 -1
- package/dist/cjs/transport.d.ts +0 -41
- package/dist/cjs/transport.d.ts.map +0 -1
- package/dist/cjs/transport.js +0 -186
- package/dist/cjs/transport.js.map +0 -1
- package/dist/esm/buffer.js +0 -79
- package/dist/esm/buffer.js.map +0 -1
- package/dist/esm/transport.js +0 -182
- package/dist/esm/transport.js.map +0 -1
- package/dist/types/buffer.d.ts +0 -33
- package/dist/types/buffer.d.ts.map +0 -1
- package/dist/types/transport.d.ts +0 -41
- package/dist/types/transport.d.ts.map +0 -1
|
@@ -1,99 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf, httpMethodOf } from './common.js';
|
|
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:
|
|
46
|
+
endpointPath: endpointPathFor(options, path, req, route),
|
|
75
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, httpMethodOf } from './common.js';
|
|
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:
|
|
46
|
+
endpointPath: endpointPathFor(options, path, request, request.routeOptions?.url ?? request.routerPath),
|
|
70
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":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9H,MAAM,qBAAqB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;AAEhF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAY,EAAE,OAA0B;IAC1E,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAChG,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;IAC9B,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAEzC,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACnE,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAE5D,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,OAAY,EAAE,KAAU,EAAE,IAAS,EAAE,EAAE;QACpE,IAAI,CAAC;YACH,MAAM,IAAI,GAAW,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC;YACxC,MAAM,MAAM,GAAW,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC;YACnD,MAAM,UAAU,GAAW,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;YAEjD,IAAI,WAAW,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YACvC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YACxE,IAAI,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YAE3D,MAAM,QAAQ,GAAG,OAAO,OAAO,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;YACnH,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC,CAAC,qCAAqC;YAEzE,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;gBACnD,CAAC,CAAC,KAAK,CAAC,WAAW;gBACnB,CAAC,CAAC,OAAO,KAAK,CAAC,eAAe,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAEtF,MAAM,CAAC,KAAK,CAAC;gBACX,UAAU,EAAE,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE;gBACzC,UAAU,EAAE,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC;gBACtD,QAAQ;gBACR,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;gBACzE,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,YAAY,EAAE,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC;gBACtG,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC;gBAChC,UAAU;gBACV,GAAG,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChF,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,6BAA6B,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qCAAqC;YACrC,OAAO,CAAC,KAAK,CAAC,6BAA8B,GAAa,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC;QAC/E,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;IAEH,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;YACpC,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,yBAAyB;AACxB,aAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,GAAG,IAAI,CAAC;AAC1D,aAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,GAAG,gBAAgB,CAAC"}
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf, httpMethodOf } from './common.js';
|
|
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:
|
|
48
|
+
endpointPath: endpointPathFor(options, path, ctx, ctx._matchedRoute ?? ctx.routerPath),
|
|
69
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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"}
|