@aforoai/metering 1.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 +88 -0
- package/dist/cjs/buffer.d.ts +27 -0
- package/dist/cjs/buffer.d.ts.map +1 -0
- package/dist/cjs/buffer.js +75 -0
- package/dist/cjs/buffer.js.map +1 -0
- package/dist/cjs/client.d.ts +86 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +301 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/idempotency.d.ts +21 -0
- package/dist/cjs/idempotency.d.ts.map +1 -0
- package/dist/cjs/idempotency.js +31 -0
- package/dist/cjs/idempotency.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/limits.d.ts +80 -0
- package/dist/cjs/limits.d.ts.map +1 -0
- package/dist/cjs/limits.js +143 -0
- package/dist/cjs/limits.js.map +1 -0
- package/dist/cjs/middleware/common.d.ts +22 -0
- package/dist/cjs/middleware/common.d.ts.map +1 -0
- package/dist/cjs/middleware/common.js +48 -0
- package/dist/cjs/middleware/common.js.map +1 -0
- package/dist/cjs/middleware/express.d.ts +15 -0
- package/dist/cjs/middleware/express.d.ts.map +1 -0
- package/dist/cjs/middleware/express.js +104 -0
- package/dist/cjs/middleware/express.js.map +1 -0
- package/dist/cjs/middleware/fastify.d.ts +13 -0
- package/dist/cjs/middleware/fastify.d.ts.map +1 -0
- package/dist/cjs/middleware/fastify.js +90 -0
- package/dist/cjs/middleware/fastify.js.map +1 -0
- package/dist/cjs/middleware/koa.d.ts +13 -0
- package/dist/cjs/middleware/koa.d.ts.map +1 -0
- package/dist/cjs/middleware/koa.js +82 -0
- package/dist/cjs/middleware/koa.js.map +1 -0
- package/dist/cjs/path-normalizer.d.ts +15 -0
- package/dist/cjs/path-normalizer.d.ts.map +1 -0
- package/dist/cjs/path-normalizer.js +49 -0
- package/dist/cjs/path-normalizer.js.map +1 -0
- package/dist/cjs/transport.d.ts +39 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +116 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/types.d.ts +128 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/buffer.js +71 -0
- package/dist/esm/buffer.js.map +1 -0
- package/dist/esm/client.js +297 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/idempotency.js +27 -0
- package/dist/esm/idempotency.js.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/limits.js +138 -0
- package/dist/esm/limits.js.map +1 -0
- package/dist/esm/middleware/common.js +41 -0
- package/dist/esm/middleware/common.js.map +1 -0
- package/dist/esm/middleware/express.js +100 -0
- package/dist/esm/middleware/express.js.map +1 -0
- package/dist/esm/middleware/fastify.js +87 -0
- package/dist/esm/middleware/fastify.js.map +1 -0
- package/dist/esm/middleware/koa.js +79 -0
- package/dist/esm/middleware/koa.js.map +1 -0
- package/dist/esm/path-normalizer.js +46 -0
- package/dist/esm/path-normalizer.js.map +1 -0
- package/dist/esm/transport.js +112 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/buffer.d.ts +27 -0
- package/dist/types/buffer.d.ts.map +1 -0
- package/dist/types/client.d.ts +86 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/idempotency.d.ts +21 -0
- package/dist/types/idempotency.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/limits.d.ts +80 -0
- package/dist/types/limits.d.ts.map +1 -0
- package/dist/types/middleware/common.d.ts +22 -0
- package/dist/types/middleware/common.d.ts.map +1 -0
- package/dist/types/middleware/express.d.ts +15 -0
- package/dist/types/middleware/express.d.ts.map +1 -0
- package/dist/types/middleware/fastify.d.ts +13 -0
- package/dist/types/middleware/fastify.d.ts.map +1 -0
- package/dist/types/middleware/koa.d.ts +13 -0
- package/dist/types/middleware/koa.d.ts.map +1 -0
- package/dist/types/path-normalizer.d.ts +15 -0
- package/dist/types/path-normalizer.d.ts.map +1 -0
- package/dist/types/transport.d.ts +39 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/types/types.d.ts +128 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AforoClient } from '../client';
|
|
2
|
+
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
|
|
3
|
+
const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
|
|
4
|
+
/**
|
|
5
|
+
* Fastify plugin that automatically captures API usage events.
|
|
6
|
+
*
|
|
7
|
+
* Uses the `onResponse` hook — runs AFTER the response is sent.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { fastifyPlugin } from '@aforoai/metering/middleware/fastify';
|
|
11
|
+
* fastify.register(fastifyPlugin, { apiKey: process.env.AFORO_API_KEY, productType: 'API' });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export async function fastifyPlugin(fastify, options) {
|
|
15
|
+
const client = new AforoClient({
|
|
16
|
+
apiKey: options.apiKey,
|
|
17
|
+
baseUrl: options.baseUrl,
|
|
18
|
+
...options.clientOptions,
|
|
19
|
+
});
|
|
20
|
+
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
21
|
+
const excludeStatusCodes = options.excludeStatusCodes ?? [];
|
|
22
|
+
fastify.addHook('onResponse', (request, reply, done) => {
|
|
23
|
+
try {
|
|
24
|
+
const path = request.url || '/';
|
|
25
|
+
const method = request.method || 'UNKNOWN';
|
|
26
|
+
const statusCode = reply.statusCode || 0;
|
|
27
|
+
if (isPreflight(method))
|
|
28
|
+
return done();
|
|
29
|
+
if (excludePaths.some((p) => path.startsWith(p)))
|
|
30
|
+
return done();
|
|
31
|
+
if (excludeStatusCodes.includes(statusCode))
|
|
32
|
+
return done();
|
|
33
|
+
const metricName = resolveMetricName(options, request, reply);
|
|
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();
|
|
54
|
+
if (!(quantity > 0))
|
|
55
|
+
return done(); // The ingestor rejects quantity <= 0
|
|
56
|
+
let metadata;
|
|
57
|
+
if (options.metadata) {
|
|
58
|
+
metadata = options.metadata(request, reply);
|
|
59
|
+
}
|
|
60
|
+
const elapsed = typeof reply.elapsedTime === 'number'
|
|
61
|
+
? reply.elapsedTime
|
|
62
|
+
: typeof reply.getResponseTime === 'function' ? reply.getResponseTime() : undefined;
|
|
63
|
+
client.track({
|
|
64
|
+
customerId,
|
|
65
|
+
metricName,
|
|
66
|
+
quantity,
|
|
67
|
+
metadata,
|
|
68
|
+
productType: options.productType,
|
|
69
|
+
endpointPath: endpointPathOf(path),
|
|
70
|
+
httpMethod: method,
|
|
71
|
+
statusCode,
|
|
72
|
+
...(typeof elapsed === 'number' ? { responseTimeMs: Math.round(elapsed) } : {}),
|
|
73
|
+
}).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Never let metering affect the API
|
|
77
|
+
}
|
|
78
|
+
done();
|
|
79
|
+
});
|
|
80
|
+
fastify.addHook('onClose', async () => {
|
|
81
|
+
await client.shutdown();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Mark as fastify plugin
|
|
85
|
+
fastifyPlugin[Symbol.for('skip-override')] = true;
|
|
86
|
+
fastifyPlugin[Symbol.for('fastify.display-name')] = 'aforo-metering';
|
|
87
|
+
//# sourceMappingURL=fastify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AforoClient } from '../client';
|
|
2
|
+
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
|
|
3
|
+
const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
|
|
4
|
+
/**
|
|
5
|
+
* Koa middleware that automatically captures API usage events.
|
|
6
|
+
*
|
|
7
|
+
* Captures after `await next()` completes — runs after the response is generated.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { koaMiddleware } from '@aforoai/metering/middleware/koa';
|
|
11
|
+
* app.use(koaMiddleware({ apiKey: process.env.AFORO_API_KEY, productType: 'API' }));
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function koaMiddleware(options) {
|
|
15
|
+
const client = new AforoClient({
|
|
16
|
+
apiKey: options.apiKey,
|
|
17
|
+
baseUrl: options.baseUrl,
|
|
18
|
+
...options.clientOptions,
|
|
19
|
+
});
|
|
20
|
+
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
21
|
+
const excludeStatusCodes = options.excludeStatusCodes ?? [];
|
|
22
|
+
return async function aforoMeteringMiddleware(ctx, next) {
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
await next();
|
|
25
|
+
try {
|
|
26
|
+
const path = ctx.path || ctx.url || '/';
|
|
27
|
+
const method = ctx.method || 'UNKNOWN';
|
|
28
|
+
const statusCode = ctx.status || 0;
|
|
29
|
+
if (isPreflight(method))
|
|
30
|
+
return;
|
|
31
|
+
if (excludePaths.some((p) => path.startsWith(p)))
|
|
32
|
+
return;
|
|
33
|
+
if (excludeStatusCodes.includes(statusCode))
|
|
34
|
+
return;
|
|
35
|
+
const metricName = resolveMetricName(options, ctx.request, ctx.response);
|
|
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;
|
|
56
|
+
if (!(quantity > 0))
|
|
57
|
+
return; // The ingestor rejects quantity <= 0
|
|
58
|
+
let metadata;
|
|
59
|
+
if (options.metadata) {
|
|
60
|
+
metadata = options.metadata(ctx.request, ctx.response);
|
|
61
|
+
}
|
|
62
|
+
client.track({
|
|
63
|
+
customerId,
|
|
64
|
+
metricName,
|
|
65
|
+
quantity,
|
|
66
|
+
metadata,
|
|
67
|
+
productType: options.productType,
|
|
68
|
+
endpointPath: endpointPathOf(path),
|
|
69
|
+
httpMethod: method,
|
|
70
|
+
statusCode,
|
|
71
|
+
responseTimeMs: Date.now() - startTime,
|
|
72
|
+
}).catch(() => { });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Never let metering affect the API
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=koa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"koa.js","sourceRoot":"","sources":["../../../src/middleware/koa.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a URL path by replacing dynamic segments with parameter placeholders.
|
|
3
|
+
*
|
|
4
|
+
* If a framework route template is available (e.g., Express `req.route?.path`),
|
|
5
|
+
* use that directly — it already has :param placeholders.
|
|
6
|
+
*
|
|
7
|
+
* Otherwise, apply heuristic normalization:
|
|
8
|
+
* /users/123 → /users/:id
|
|
9
|
+
* /orders/abc-def → /orders/:id
|
|
10
|
+
* /v1/items/42/reviews/7 → /v1/items/:id/reviews/:id
|
|
11
|
+
*
|
|
12
|
+
* UUIDs, numeric IDs, and alphanumeric slugs are detected and replaced.
|
|
13
|
+
*/
|
|
14
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
const NUMERIC_RE = /^\d+$/;
|
|
16
|
+
const MONGO_ID_RE = /^[0-9a-f]{24}$/i;
|
|
17
|
+
// Version-like segments: v1, v2, v10, etc. — should NOT be normalized
|
|
18
|
+
const VERSION_RE = /^v\d+$/i;
|
|
19
|
+
export function normalizePath(actualPath, routeTemplate) {
|
|
20
|
+
// Prefer the framework's route template if available
|
|
21
|
+
if (routeTemplate) {
|
|
22
|
+
return routeTemplate;
|
|
23
|
+
}
|
|
24
|
+
// Heuristic: replace dynamic-looking segments
|
|
25
|
+
const segments = actualPath.split('/');
|
|
26
|
+
const normalized = segments.map((seg) => {
|
|
27
|
+
if (!seg)
|
|
28
|
+
return seg;
|
|
29
|
+
// Preserve version segments (v1, v2, v10)
|
|
30
|
+
if (VERSION_RE.test(seg))
|
|
31
|
+
return seg;
|
|
32
|
+
if (UUID_RE.test(seg))
|
|
33
|
+
return ':id';
|
|
34
|
+
if (NUMERIC_RE.test(seg))
|
|
35
|
+
return ':id';
|
|
36
|
+
if (MONGO_ID_RE.test(seg))
|
|
37
|
+
return ':id';
|
|
38
|
+
// Alphanumeric tokens > 4 chars that mix letters and digits look like IDs
|
|
39
|
+
if (seg.length > 4 && seg.length <= 12 && /^[a-zA-Z0-9_-]+$/.test(seg) && /\d/.test(seg) && /[a-zA-Z]/.test(seg)) {
|
|
40
|
+
return ':id';
|
|
41
|
+
}
|
|
42
|
+
return seg;
|
|
43
|
+
});
|
|
44
|
+
return normalized.join('/');
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=path-normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-normalizer.js","sourceRoot":"","sources":["../../src/path-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAClF,MAAM,UAAU,GAAG,OAAO,CAAC;AAC3B,MAAM,WAAW,GAAG,iBAAiB,CAAC;AACtC,sEAAsE;AACtE,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B,MAAM,UAAU,aAAa,CAC3B,UAAkB,EAClB,aAAsB;IAEtB,qDAAqD;IACrD,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,8CAA8C;IAC9C,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACtC,IAAI,CAAC,GAAG;YAAE,OAAO,GAAG,CAAC;QACrB,0CAA0C;QAC1C,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC;QACrC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACpC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACvC,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACxC,0EAA0E;QAC1E,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACjH,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport that sends batched usage events to the Aforo ingestor.
|
|
3
|
+
*
|
|
4
|
+
* - POST /v1/ingest/batch with X-API-Key: {apiKey} (never Authorization: Bearer -- the ingestor parses Bearer as a JWT and rejects the request 401)
|
|
5
|
+
* - Retry on 5xx, 408, 429 with exponential backoff
|
|
6
|
+
* - Respects Retry-After header on 429
|
|
7
|
+
* - No retry on 4xx (bad input)
|
|
8
|
+
* - AbortController timeout per request
|
|
9
|
+
*/
|
|
10
|
+
export class Transport {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
13
|
+
this.apiKey = options.apiKey;
|
|
14
|
+
this.timeout = options.timeout;
|
|
15
|
+
this.maxRetries = options.maxRetries;
|
|
16
|
+
this.retryBaseMs = options.retryBaseMs;
|
|
17
|
+
}
|
|
18
|
+
/** Send a batch of events. Returns the number sent and failed. */
|
|
19
|
+
async send(events) {
|
|
20
|
+
if (events.length === 0)
|
|
21
|
+
return { sent: 0, failed: 0 };
|
|
22
|
+
const url = `${this.baseUrl}/v1/ingest/batch`;
|
|
23
|
+
const body = { events };
|
|
24
|
+
const bodyStr = JSON.stringify(body);
|
|
25
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'X-API-Key': this.apiKey,
|
|
34
|
+
},
|
|
35
|
+
body: bodyStr,
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
});
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
if (response.ok) {
|
|
40
|
+
return { sent: events.length, failed: 0 };
|
|
41
|
+
}
|
|
42
|
+
// 4xx (except 408, 429) — bad input, don't retry
|
|
43
|
+
if (response.status >= 400 && response.status < 500
|
|
44
|
+
&& response.status !== 408 && response.status !== 429) {
|
|
45
|
+
return { sent: 0, failed: events.length };
|
|
46
|
+
}
|
|
47
|
+
// 429 — respect Retry-After header
|
|
48
|
+
if (response.status === 429) {
|
|
49
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
50
|
+
const delayMs = retryAfter
|
|
51
|
+
? (parseInt(retryAfter, 10) || 1) * 1000
|
|
52
|
+
: this.retryBaseMs * Math.pow(2, attempt);
|
|
53
|
+
if (attempt < this.maxRetries) {
|
|
54
|
+
await this.sleep(delayMs);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// 5xx or 408 — retry with backoff
|
|
59
|
+
if (attempt < this.maxRetries) {
|
|
60
|
+
await this.sleep(this.retryBaseMs * Math.pow(2, attempt));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
return { sent: 0, failed: events.length };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Network error or abort — retry
|
|
67
|
+
if (attempt < this.maxRetries) {
|
|
68
|
+
await this.sleep(this.retryBaseMs * Math.pow(2, attempt));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
return { sent: 0, failed: events.length };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { sent: 0, failed: events.length };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Send a single event in its own request, once, best-effort.
|
|
78
|
+
*
|
|
79
|
+
* Used for session heartbeats: a heartbeat must never share a batch with
|
|
80
|
+
* usage (a large batch takes the high-throughput path, which does not
|
|
81
|
+
* intercept heartbeats), and a lost heartbeat is simply superseded by the
|
|
82
|
+
* next one -- so there is no retry and every failure is swallowed.
|
|
83
|
+
* Resolves to true when the ingestor accepted the request.
|
|
84
|
+
*/
|
|
85
|
+
async sendSingleBestEffort(event) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
88
|
+
try {
|
|
89
|
+
const body = { events: [event] };
|
|
90
|
+
const response = await fetch(`${this.baseUrl}/v1/ingest/batch`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'X-API-Key': this.apiKey,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(body),
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
return response.ok;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
sleep(ms) {
|
|
109
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ResolvedEvent } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Bounded ring buffer for usage events.
|
|
4
|
+
* Thread-safe for Node.js single-threaded event loop.
|
|
5
|
+
* When full, the oldest event is dropped (FIFO overflow).
|
|
6
|
+
*/
|
|
7
|
+
export declare class RingBuffer {
|
|
8
|
+
private readonly items;
|
|
9
|
+
private readonly capacity;
|
|
10
|
+
private head;
|
|
11
|
+
private tail;
|
|
12
|
+
private count;
|
|
13
|
+
constructor(capacity?: number);
|
|
14
|
+
/** Add an event to the buffer. Returns true if added, false if overflow (oldest dropped). */
|
|
15
|
+
push(event: ResolvedEvent): boolean;
|
|
16
|
+
/** Drain all events from the buffer. Returns a new array and clears the buffer. */
|
|
17
|
+
drain(): ResolvedEvent[];
|
|
18
|
+
/** Drain up to `max` events from the front of the buffer. */
|
|
19
|
+
drainUpTo(max: number): ResolvedEvent[];
|
|
20
|
+
/** Current number of events in the buffer. */
|
|
21
|
+
get size(): number;
|
|
22
|
+
/** Whether the buffer is empty. */
|
|
23
|
+
get isEmpty(): boolean;
|
|
24
|
+
/** Whether the buffer is at capacity. */
|
|
25
|
+
get isFull(): boolean;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/buffer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExC;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAK;gBAEN,QAAQ,GAAE,MAAe;IAMrC,6FAA6F;IAC7F,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO;IAiBnC,mFAAmF;IACnF,KAAK,IAAI,aAAa,EAAE;IAgBxB,6DAA6D;IAC7D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE;IAgBvC,8CAA8C;IAC9C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mCAAmC;IACnC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,yCAAyC;IACzC,IAAI,MAAM,IAAI,OAAO,CAEpB;CACF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AforoOptions, TrackEvent, FlushResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Aforo metering client.
|
|
4
|
+
*
|
|
5
|
+
* Enqueues usage events into a ring buffer, flushes them in batches
|
|
6
|
+
* to the Aforo ingestor service via HTTP. Non-blocking — `track()`
|
|
7
|
+
* returns immediately, flushing happens in the background.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const client = new AforoClient({ apiKey: 'your-key', productType: 'API' });
|
|
11
|
+
* await client.track({ customerId: 'cust_1', metricName: 'api_calls', quantity: 1 });
|
|
12
|
+
* // On shutdown:
|
|
13
|
+
* await client.shutdown();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare class AforoClient {
|
|
17
|
+
private readonly buffer;
|
|
18
|
+
private readonly transport;
|
|
19
|
+
private readonly flushCount;
|
|
20
|
+
private readonly flushInterval;
|
|
21
|
+
private readonly shutdownTimeoutMs;
|
|
22
|
+
private readonly productType;
|
|
23
|
+
private flushTimer;
|
|
24
|
+
private flushing;
|
|
25
|
+
private closed;
|
|
26
|
+
private pendingFlush;
|
|
27
|
+
private heartbeatTimer;
|
|
28
|
+
private activeSessionId;
|
|
29
|
+
private sessionStartedAt;
|
|
30
|
+
private sessionProductType;
|
|
31
|
+
constructor(options: AforoOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Start a session and emit `system.session.heartbeat` events: one now, then
|
|
34
|
+
* every 30s until `endSession()` / `shutdown()`.
|
|
35
|
+
*
|
|
36
|
+
* Each heartbeat is POSTed in its own request (`{"events":[heartbeat]}`),
|
|
37
|
+
* never mixed into a usage batch, so it always takes the ingestor's
|
|
38
|
+
* synchronous path, where it is intercepted before billing. Heartbeats are
|
|
39
|
+
* best-effort: sent once, failures ignored, never affecting usage delivery.
|
|
40
|
+
* The timer is unref'd so it never keeps the process alive.
|
|
41
|
+
*
|
|
42
|
+
* @param sessionId - Unique session identifier
|
|
43
|
+
* @param productType - Product type for the session (default AI_AGENT)
|
|
44
|
+
*/
|
|
45
|
+
startSession(sessionId: string, productType?: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* End the current session: stop heartbeats, send a final SESSION_END
|
|
48
|
+
* heartbeat (in its own request, best-effort) and flush buffered usage.
|
|
49
|
+
*/
|
|
50
|
+
endSession(): Promise<void>;
|
|
51
|
+
private stopHeartbeatTimer;
|
|
52
|
+
private emitSessionHeartbeat;
|
|
53
|
+
/**
|
|
54
|
+
* Enqueue a usage event for batched delivery.
|
|
55
|
+
* Returns immediately — does not await HTTP.
|
|
56
|
+
* Triggers a flush if the buffer reaches flushCount.
|
|
57
|
+
*
|
|
58
|
+
* Throws if the event breaks any constraint the ingestor enforces — a blank
|
|
59
|
+
* or over-long `customerId` / `metricName` / `idempotencyKey`, a `quantity`
|
|
60
|
+
* that is not positive or carries more than 14 integer digits / 6 decimal
|
|
61
|
+
* places, an `occurredAt` that is not an ISO-8601 instant, or any other
|
|
62
|
+
* capped field that is too long. Limits the server makes configurable (event
|
|
63
|
+
* age, clock skew, metadata size) are left to the server, so the SDK can
|
|
64
|
+
* never refuse usage a given deployment would accept. Such an event is rejected by the ingestor and never billed,
|
|
65
|
+
* and because flushing happens in the background nobody would see that
|
|
66
|
+
* rejection — so it is reported here instead, and never enters the buffer.
|
|
67
|
+
* See `limits.ts` for the limits and where each one comes from.
|
|
68
|
+
*/
|
|
69
|
+
track(event: TrackEvent): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Force-flush all buffered events to the ingestor.
|
|
72
|
+
* Safe to call concurrently — only one flush runs at a time.
|
|
73
|
+
*/
|
|
74
|
+
flush(): Promise<FlushResult>;
|
|
75
|
+
private doFlush;
|
|
76
|
+
/**
|
|
77
|
+
* Flush remaining events and stop the client.
|
|
78
|
+
* After shutdown, track() will throw.
|
|
79
|
+
*/
|
|
80
|
+
shutdown(): Promise<void>;
|
|
81
|
+
/** Number of events currently buffered. */
|
|
82
|
+
get bufferedCount(): number;
|
|
83
|
+
/** Whether the client has been shut down. */
|
|
84
|
+
get isShutdown(): boolean;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAiB,WAAW,EAAE,MAAM,SAAS,CAAC;AAuB/E;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAqC;IAGzD,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,kBAAkB,CAAwC;gBAEtD,OAAO,EAAE,YAAY;IAsCjC;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,GAAE,MAAqC,GAAG,IAAI;IAezF;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQjC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,oBAAoB;IAgC5B;;;;;;;;;;;;;;;OAeG;IACG,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA+D7C;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;YAgBrB,OAAO;IAgBrB;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB/B,2CAA2C;IAC3C,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,6CAA6C;IAC7C,IAAI,UAAU,IAAI,OAAO,CAExB;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a deterministic idempotency key from event fields.
|
|
3
|
+
* Uses SHA-256, truncated to 32 hex chars for compact storage.
|
|
4
|
+
*
|
|
5
|
+
* Formula: SHA256(customerId + metricName + quantity + occurredAt)
|
|
6
|
+
*
|
|
7
|
+
* NOT the client default any more. `occurredAt` only carries millisecond
|
|
8
|
+
* precision, so two genuinely distinct events for the same customer + metric
|
|
9
|
+
* + quantity inside one millisecond hash to the same key and the ingestor
|
|
10
|
+
* drops the second as a DUPLICATE — silent under-billing. Kept exported for
|
|
11
|
+
* callers who deliberately want content-addressed dedup (e.g. replaying a
|
|
12
|
+
* fixed batch) and pass the result to `track({ idempotencyKey })` themselves.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateIdempotencyKey(customerId: string, metricName: string, quantity: number, occurredAt: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a random UUID v4. This is the default key for an event whose caller
|
|
17
|
+
* supplied none: every event gets its own key, so no two distinct events can
|
|
18
|
+
* collide. Dedup stays opt-in via an explicit `idempotencyKey`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateRandomKey(): string;
|
|
21
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/idempotency.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AforoClient } from './client';
|
|
2
|
+
export { expressMiddleware, middleware } from './middleware/express';
|
|
3
|
+
export { fastifyPlugin } from './middleware/fastify';
|
|
4
|
+
export { koaMiddleware } from './middleware/koa';
|
|
5
|
+
export { normalizePath } from './path-normalizer';
|
|
6
|
+
export { DEFAULT_METRIC_NAME } from './middleware/common';
|
|
7
|
+
export { MAX_LENGTHS, MAX_QUANTITY_DECIMAL_PLACES, MAX_QUANTITY_INTEGER_DIGITS, describeLimitViolation, } from './limits';
|
|
8
|
+
export type { AforoOptions, TrackEvent, MiddlewareOptions, FlushResult, BatchRequest, BatchResponse, } from './types';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EACL,WAAW,EACX,2BAA2B,EAC3B,2BAA2B,EAC3B,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side mirror of the ingestor's per-event field constraints.
|
|
3
|
+
*
|
|
4
|
+
* An event that breaks one of these is rejected by the ingestor and never
|
|
5
|
+
* billed. The server reports it per event (`errors[]` in the batch response,
|
|
6
|
+
* indexed), so the rest of the batch survives — but the SDK sends in the
|
|
7
|
+
* background, so that report reaches nobody: the usage is simply gone. Checking
|
|
8
|
+
* the same limits at `track()` surfaces the problem to the caller, at the call
|
|
9
|
+
* site that produced it, while the event can still be corrected.
|
|
10
|
+
*
|
|
11
|
+
* Source: `dto/IngestUsageEventRequest` (aforo-nextgen-usage-ingestor-service) —
|
|
12
|
+
* the `@Size` and `@Digits` bean constraints, which are compiled into the
|
|
13
|
+
* server and therefore identical in every environment.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT mirrored here: the timestamp window (`max-age-days`,
|
|
16
|
+
* `future-tolerance-minutes`) and the metadata cap (`max-metadata-bytes`) in
|
|
17
|
+
* `validation/UsageEventValidator`. Each is a per-environment property — a
|
|
18
|
+
* tenant may raise `max-age-days` to 365 for backfills — so enforcing the
|
|
19
|
+
* default here would make the SDK refuse usage its own server would accept and
|
|
20
|
+
* bill. Refusing real usage is a worse failure than the rejection it prevents.
|
|
21
|
+
* Only `occurredAt`'s shape is checked, since a malformed timestamp is invalid
|
|
22
|
+
* under every configuration.
|
|
23
|
+
*
|
|
24
|
+
* Nothing here truncates or rounds — that would change what is billed. The
|
|
25
|
+
* offending event is rejected instead, naming the field, the limit, and the
|
|
26
|
+
* offending value.
|
|
27
|
+
*/
|
|
28
|
+
/** Maximum characters the ingestor accepts per string field (`@Size(max = ...)`). */
|
|
29
|
+
export declare const MAX_LENGTHS: {
|
|
30
|
+
readonly customerId: 64;
|
|
31
|
+
readonly metricName: 255;
|
|
32
|
+
readonly idempotencyKey: 255;
|
|
33
|
+
readonly productType: 20;
|
|
34
|
+
readonly traceId: 128;
|
|
35
|
+
readonly spanId: 32;
|
|
36
|
+
readonly sessionId: 64;
|
|
37
|
+
readonly agentId: 36;
|
|
38
|
+
readonly toolName: 64;
|
|
39
|
+
readonly endpointPath: 512;
|
|
40
|
+
readonly httpMethod: 16;
|
|
41
|
+
};
|
|
42
|
+
/** `@Digits(integer = 14, fraction = 6)` — `usage_events.quantity` is NUMERIC(20,6). */
|
|
43
|
+
export declare const MAX_QUANTITY_INTEGER_DIGITS = 14;
|
|
44
|
+
export declare const MAX_QUANTITY_DECIMAL_PLACES = 6;
|
|
45
|
+
/**
|
|
46
|
+
* Integer and decimal digit counts as the ingestor's `@Digits` sees them, i.e.
|
|
47
|
+
* as `BigDecimal` reads the number the SDK puts on the wire:
|
|
48
|
+
* `integerDigits = precision - scale`, `decimalPlaces = max(scale, 0)`.
|
|
49
|
+
*
|
|
50
|
+
* Works from the serialized text so exponent notation (`1e+21`, which
|
|
51
|
+
* `JSON.stringify` emits for large numbers) is counted the same way the server
|
|
52
|
+
* will count it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function quantityDigits(serialized: string): {
|
|
55
|
+
integerDigits: number;
|
|
56
|
+
decimalPlaces: number;
|
|
57
|
+
};
|
|
58
|
+
/** The already-resolved event fields the limits apply to. */
|
|
59
|
+
export interface LimitCheckedEvent {
|
|
60
|
+
customerId?: unknown;
|
|
61
|
+
metricName?: unknown;
|
|
62
|
+
quantity: number;
|
|
63
|
+
idempotencyKey?: unknown;
|
|
64
|
+
occurredAt: string;
|
|
65
|
+
productType?: unknown;
|
|
66
|
+
endpointPath?: unknown;
|
|
67
|
+
httpMethod?: unknown;
|
|
68
|
+
traceId?: unknown;
|
|
69
|
+
spanId?: unknown;
|
|
70
|
+
sessionId?: unknown;
|
|
71
|
+
agentId?: unknown;
|
|
72
|
+
toolName?: unknown;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Describe the first ingestor constraint this event breaks, or `null` when it
|
|
76
|
+
* would be accepted. Callers that must not throw (the framework middlewares)
|
|
77
|
+
* can use this to skip an event instead.
|
|
78
|
+
*/
|
|
79
|
+
export declare function describeLimitViolation(event: LimitCheckedEvent): string | null;
|
|
80
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,qFAAqF;AACrF,eAAO,MAAM,WAAW;;;;;;;;;;;;CAYd,CAAC;AAEX,wFAAwF;AACxF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAoC7C;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAYnG;AA+BD,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAe9E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MiddlewareOptions } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Metric recorded for each request when `metricName` is not configured.
|
|
4
|
+
*
|
|
5
|
+
* It must exist in the tenant's Aforo metric catalog: the ingestor rejects an
|
|
6
|
+
* unknown metric, and because it validates a batch as a whole, one such event
|
|
7
|
+
* fails the entire batch with 400. The previous default, `"METHOD /path"`, is a
|
|
8
|
+
* name no catalog contains, so every event failed out of the box.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_METRIC_NAME = "api_calls";
|
|
11
|
+
/**
|
|
12
|
+
* CORS preflights are a browser protocol detail, not a billable call, and carry
|
|
13
|
+
* no credentials -- so they never have a customer. They are never metered.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isPreflight(method: string | undefined): boolean;
|
|
16
|
+
/** Resolve the metric: resolver function, then fixed name, then DEFAULT_METRIC_NAME. */
|
|
17
|
+
export declare function resolveMetricName(options: MiddlewareOptions, req: any, res: any): string;
|
|
18
|
+
/** Request path without its query string, capped to what the ingestor accepts. */
|
|
19
|
+
export declare function endpointPathOf(url: string): string;
|
|
20
|
+
/** First non-empty string among the candidates, or null. */
|
|
21
|
+
export declare function firstNonEmpty(...values: unknown[]): string | null;
|
|
22
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE/D;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,CAKxF;AAKD,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CAOjE"}
|