Skip to content

Commit 4d3d6fc

Browse files
authored
fix: improve errors for usage processor 2 format (#6472)
1 parent 829df31 commit 4d3d6fc

File tree

4 files changed

+137
-19
lines changed

4 files changed

+137
-19
lines changed

.changeset/small-readers-melt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'hive': patch
3+
---
4+
5+
Improve the usage reporting endpoint error responses to include all the errors for invalid JSON
6+
bodies.

packages/services/usage/__tests__/usage-processor-2-validation.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,3 +1009,50 @@ test('$.operations.execution.errorsTotal is required', () => {
10091009
success: false,
10101010
});
10111011
});
1012+
1013+
test('invalid duration and timestamp (-1) gives helpful error response', () => {
1014+
expect(
1015+
decodeReport({
1016+
size: 1,
1017+
map: {
1018+
op1Key: {
1019+
operation: `query op1Name { field1 }`,
1020+
fields: ['Query.field1'],
1021+
},
1022+
},
1023+
operations: [
1024+
{
1025+
operationMapKey: 'op1Key',
1026+
timestamp: -1,
1027+
execution: {
1028+
ok: true,
1029+
duration: -1,
1030+
errorsTotal: 1,
1031+
},
1032+
},
1033+
],
1034+
}),
1035+
).toEqual({
1036+
errors: [
1037+
{
1038+
errors: [
1039+
{
1040+
message: 'Expected valid unix timestamp in milliseconds',
1041+
path: '/operations/0/timestamp',
1042+
},
1043+
{
1044+
message: 'Expected integer to be greater or equal to 0',
1045+
path: '/operations/0/execution/duration',
1046+
},
1047+
{
1048+
message: 'Expected null',
1049+
path: '/operations',
1050+
},
1051+
],
1052+
message: 'Expected union value',
1053+
path: '/operations',
1054+
},
1055+
],
1056+
success: false,
1057+
});
1058+
});

packages/services/usage/src/usage-processor-2.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@hive/usage-common';
99
import * as tb from '@sinclair/typebox';
1010
import * as tc from '@sinclair/typebox/compiler';
11+
import * as tbe from '@sinclair/typebox/errors';
1112
import { invalidRawOperations, rawOperationsSize, totalOperations, totalReports } from './metrics';
1213
import { TokensResponse } from './tokens';
1314
import { isValidOperationBody } from './usage-processor-1';
@@ -34,7 +35,7 @@ export const usageProcessorV2 = traceInlineSync(
3435
token: TokensResponse,
3536
targetRetentionInDays: number | null,
3637
):
37-
| { success: false; errors: Array<tc.ValueError> }
38+
| { success: false; errors: Array<ValueError> }
3839
| {
3940
success: true;
4041
report: RawReport;
@@ -307,6 +308,12 @@ function isUnixTimestamp(x: number) {
307308
return unixTimestampRegex.test(String(x));
308309
}
309310

311+
tbe.SetErrorFunction(param => {
312+
return param.schema[tb.Kind] === 'UnixTimestampInMs'
313+
? 'Expected valid unix timestamp in milliseconds'
314+
: tbe.DefaultErrorFunction(param);
315+
});
316+
310317
tb.TypeRegistry.Set<number>('UnixTimestampInMs', (_, value) =>
311318
typeof value === 'number' ? isUnixTimestamp(value) : false,
312319
);
@@ -361,14 +368,20 @@ type ReportType = tb.Static<typeof ReportSchema>;
361368

362369
const ReportModel = tc.TypeCompiler.Compile(ReportSchema);
363370

371+
interface ValueError {
372+
path: string;
373+
message: string;
374+
errors?: ValueError[];
375+
}
376+
364377
export function decodeReport(
365378
report: unknown,
366-
): { success: true; report: ReportType } | { success: false; errors: tc.ValueError[] } {
367-
const errors = getFirstN(ReportModel.Errors(report), 5);
368-
if (errors.length) {
379+
): { success: true; report: ReportType } | { success: false; errors: Array<ValueError> } {
380+
const errors = ReportModel.Errors(report);
381+
if (ReportModel.Errors(report).First()) {
369382
return {
370383
success: false,
371-
errors,
384+
errors: getTypeBoxErrors(errors),
372385
};
373386
}
374387

@@ -378,19 +391,15 @@ export function decodeReport(
378391
};
379392
}
380393

381-
function getFirstN<TValue>(iterable: Iterable<TValue>, max: number): TValue[] {
382-
let counter = 0;
383-
const items: Array<TValue> = [];
384-
for (const item of iterable) {
385-
items.push(item);
386-
counter++;
387-
388-
if (counter >= max) {
389-
break;
390-
}
391-
}
392-
393-
return items;
394+
function getTypeBoxErrors(errors: tc.ValueErrorIterator): Array<ValueError> {
395+
return Array.from(errors).map(error => {
396+
const errors = error.errors.flatMap(errors => getTypeBoxErrors(errors));
397+
return {
398+
path: error.path,
399+
message: error.message,
400+
errors: errors.length ? errors : undefined,
401+
};
402+
});
394403
}
395404

396405
const DAY_IN_MS = 86_400_000;

packages/web/docs/src/content/specs/usage-reports.mdx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ them in batches (as a single report, when a buffer is full or every few seconds)
1919
| Authorization Header | `Authorization: Bearer token-here` |
2020
| API version Header | `X-Usage-API-Version: 2` |
2121
| Method | `POST` |
22+
| Content-Type Header | `Content-Type: application/json` |
2223

23-
## Data structure
24+
## JSON Body structure
2425

2526
<details>
2627
<summary>TypeScript schema</summary>
@@ -193,3 +194,58 @@ curl -X POST \
193194
-H 'content-type: application/json' \
194195
-d '{ "size": 1, "map": { "aaa": { "operationName": "me", "operation": "query me { me { id } }", "fields": ["Query", "Query.me", "User", "User.id"] } }, "operations": [{ "operationMapKey" : "c3b6d9b0", "timestamp" : 1663158676535, "execution" : { "ok" : true, "duration" : 150000000, "errorsTotal" : 0 }, "metadata" : { "client" : { "name" : "demo" , "version" : "0.0.1" } } } ] }'
195196
```
197+
198+
## Response
199+
200+
| Status Code | Meaning |
201+
| ----------- | ---------------------------------------------------- |
202+
| `200` | Usage data was successfully accepted. |
203+
| `400` | Errors while processing the sent JSON body. |
204+
| `401` | Invalid `X-Usage-API-Version` header provided. |
205+
| `429` | Rate limited due to exceeding usage reporting quota. |
206+
| `500` | An unexpected error occured. |
207+
208+
The endpoint will return a JSON body response body for `200` and `400` status codes.
209+
210+
### 200 Status Body
211+
212+
```json filename="Response 200"
213+
{
214+
"id": "c6ba1f9c-44c0-40a1-8089-65f7e4de5de5",
215+
"operations": {
216+
"accepted": 20,
217+
"rejected": 0
218+
}
219+
}
220+
```
221+
222+
### 400 Status Body
223+
224+
A response with status 400 indicates that the report sent within the request body is not valid. The
225+
response body will contain a JSON Schema validation errors that can be used to debug the faulty
226+
request body.
227+
228+
```json filename="Response 400"
229+
{
230+
"errors": [
231+
{
232+
"message": "Expected union value",
233+
"path": "/operations",
234+
"errors": [
235+
{
236+
"message": "Expected valid unix timestamp in milliseconds",
237+
"path": "/operations/0/timestamp"
238+
},
239+
{
240+
"message": "Expected integer to be greater or equal to 0",
241+
"path": "/operations/0/execution/duration"
242+
},
243+
{
244+
"message": "Expected null",
245+
"path": "/operations"
246+
}
247+
]
248+
}
249+
]
250+
}
251+
```

0 commit comments

Comments
 (0)