@slates/provider 1.0.0-rc.6 → 1.0.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1588 -2
- package/dist/index.d.cts +602 -0
- package/dist/index.d.ts +602 -9
- package/dist/index.module.js +1519 -2
- package/package.json +8 -6
- package/src/action/action.ts +16 -3
- package/src/action/builder.ts +21 -4
- package/src/action/index.ts +2 -2
- package/src/action/scopes.test.ts +77 -0
- package/src/action/scopes.ts +101 -0
- package/src/action/tool.ts +8 -4
- package/src/action/trigger.ts +11 -11
- package/src/auth/auth.ts +3 -3
- package/src/auth/types.ts +7 -6
- package/src/axios/index.ts +56 -9
- package/src/axios/trace.ts +443 -0
- package/src/config/config.ts +3 -3
- package/src/context/context.ts +29 -2
- package/src/context/hook.ts +1 -1
- package/src/error/axios.ts +219 -0
- package/src/error/base.test.ts +178 -0
- package/src/error/base.ts +108 -2
- package/src/error/declaration.ts +7 -2
- package/src/error/defaults.ts +50 -0
- package/src/error/index.ts +3 -0
- package/src/error/service.ts +160 -0
- package/src/error/types.ts +73 -0
- package/src/error/utils.ts +155 -0
- package/src/logger/logger.test.ts +57 -0
- package/src/logger/logger.ts +72 -11
- package/src/specification/slate.ts +2 -2
- package/src/specification/specification.ts +2 -2
- package/dist/action/action.d.ts +0 -98
- package/dist/action/action.d.ts.map +0 -1
- package/dist/action/builder.d.ts +0 -27
- package/dist/action/builder.d.ts.map +0 -1
- package/dist/action/index.d.ts +0 -5
- package/dist/action/index.d.ts.map +0 -1
- package/dist/action/tool.d.ts +0 -14
- package/dist/action/tool.d.ts.map +0 -1
- package/dist/action/trigger.d.ts +0 -32
- package/dist/action/trigger.d.ts.map +0 -1
- package/dist/auth/auth.d.ts +0 -30
- package/dist/auth/auth.d.ts.map +0 -1
- package/dist/auth/index.d.ts +0 -3
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/types.d.ts +0 -150
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/axios/index.d.ts +0 -4
- package/dist/axios/index.d.ts.map +0 -1
- package/dist/config/config.d.ts +0 -26
- package/dist/config/config.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -2
- package/dist/config/index.d.ts.map +0 -1
- package/dist/context/context.d.ts +0 -20
- package/dist/context/context.d.ts.map +0 -1
- package/dist/context/hook.d.ts +0 -4
- package/dist/context/hook.d.ts.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/error/base.d.ts +0 -5
- package/dist/error/base.d.ts.map +0 -1
- package/dist/error/declaration.d.ts +0 -6
- package/dist/error/declaration.d.ts.map +0 -1
- package/dist/error/index.d.ts +0 -3
- package/dist/error/index.d.ts.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.modern.js +0 -2
- package/dist/index.modern.js.map +0 -1
- package/dist/index.module.js.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/dist/logger/index.d.ts +0 -2
- package/dist/logger/index.d.ts.map +0 -1
- package/dist/logger/logger.d.ts +0 -20
- package/dist/logger/logger.d.ts.map +0 -1
- package/dist/specification/index.d.ts +0 -3
- package/dist/specification/index.d.ts.map +0 -1
- package/dist/specification/slate.d.ts +0 -15
- package/dist/specification/slate.d.ts.map +0 -1
- package/dist/specification/specification.d.ts +0 -30
- package/dist/specification/specification.d.ts.map +0 -1
- package/dist/specification/zero.d.ts +0 -13
- package/dist/specification/zero.d.ts.map +0 -1
- package/dist/tokens.d.ts +0 -34
- package/dist/tokens.d.ts.map +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { isServiceError } from '@lowerdeck/error';
|
|
2
|
+
import type { ErrorData, ErrorRecord, ServiceError } from '@lowerdeck/error';
|
|
3
|
+
import { DEFAULT_CODE, SERVICE_ERROR_CODE_MAP } from './defaults';
|
|
4
|
+
import type { SlateErrorInput, SlateErrorIssue, SlateServiceErrorData } from './types';
|
|
5
|
+
import { cleanRecord, isRecord, normalizeKind, normalizeRetryable, sanitizeForBaggage } from './utils';
|
|
6
|
+
|
|
7
|
+
let SERVICE_ERROR_DATA_RESERVED_KEYS = new Set([
|
|
8
|
+
'status',
|
|
9
|
+
'code',
|
|
10
|
+
'message',
|
|
11
|
+
'hint',
|
|
12
|
+
'description',
|
|
13
|
+
'reason',
|
|
14
|
+
'errors',
|
|
15
|
+
'object',
|
|
16
|
+
'ok'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
let isStructuredValue = (value: unknown): value is Record<string, unknown> | Function =>
|
|
20
|
+
(typeof value === 'object' || typeof value === 'function') && value !== null;
|
|
21
|
+
|
|
22
|
+
let mapServiceErrorCodeByStatus = (status?: number) => {
|
|
23
|
+
if (status === 400) return 'request.bad';
|
|
24
|
+
if (status === 401) return 'auth.required';
|
|
25
|
+
if (status === 402) return 'payment.required';
|
|
26
|
+
if (status === 403) return 'permission.denied';
|
|
27
|
+
if (status === 404) return 'resource.not_found';
|
|
28
|
+
if (status === 406) return 'request.not_acceptable';
|
|
29
|
+
if (status === 409) return 'resource.conflict';
|
|
30
|
+
if (status === 410) return 'resource.gone';
|
|
31
|
+
if (status === 412) return 'request.precondition_failed';
|
|
32
|
+
if (status === 429) return 'request.rate_limited';
|
|
33
|
+
if (status === 501) return 'operation.not_implemented';
|
|
34
|
+
if (status === 504) return 'internal.timeout';
|
|
35
|
+
if (status && status >= 500) return 'internal.unexpected';
|
|
36
|
+
return DEFAULT_CODE;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let normalizeServiceErrorIssues = (errors: unknown): SlateErrorIssue[] | undefined => {
|
|
40
|
+
if (!Array.isArray(errors)) return undefined;
|
|
41
|
+
|
|
42
|
+
let issues = errors.map(error => {
|
|
43
|
+
if (!isRecord(error)) {
|
|
44
|
+
return {
|
|
45
|
+
message: String(error)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let path = Array.isArray(error.path)
|
|
50
|
+
? error.path.map(entry => String(entry))
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...error,
|
|
55
|
+
path,
|
|
56
|
+
code: typeof error.code === 'string' ? error.code : undefined,
|
|
57
|
+
message:
|
|
58
|
+
typeof error.message === 'string'
|
|
59
|
+
? error.message
|
|
60
|
+
: typeof error.hint === 'string'
|
|
61
|
+
? error.hint
|
|
62
|
+
: 'Validation issue'
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return issues.length > 0 ? issues : undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let getServiceErrorBaggage = (data: SlateServiceErrorData) => {
|
|
70
|
+
let extraEntries = Object.entries(data).filter(([key]) => !SERVICE_ERROR_DATA_RESERVED_KEYS.has(key));
|
|
71
|
+
|
|
72
|
+
return cleanRecord({
|
|
73
|
+
serviceError: {
|
|
74
|
+
code: data.code,
|
|
75
|
+
status: data.status,
|
|
76
|
+
hint: data.hint,
|
|
77
|
+
description: data.description,
|
|
78
|
+
reason: data.reason
|
|
79
|
+
},
|
|
80
|
+
...(extraEntries.length > 0
|
|
81
|
+
? {
|
|
82
|
+
serviceErrorData: sanitizeForBaggage(Object.fromEntries(extraEntries))
|
|
83
|
+
}
|
|
84
|
+
: {})
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export let mapServiceErrorCode = (code: string, status?: number) =>
|
|
89
|
+
SERVICE_ERROR_CODE_MAP[code] ?? mapServiceErrorCodeByStatus(status);
|
|
90
|
+
|
|
91
|
+
export let isServiceErrorData = (value: unknown): value is SlateServiceErrorData =>
|
|
92
|
+
isRecord(value) &&
|
|
93
|
+
typeof value.status === 'number' &&
|
|
94
|
+
typeof value.code === 'string' &&
|
|
95
|
+
typeof value.message === 'string' &&
|
|
96
|
+
!('config' in value) &&
|
|
97
|
+
!('request' in value) &&
|
|
98
|
+
!('response' in value) &&
|
|
99
|
+
!('isAxiosError' in value);
|
|
100
|
+
|
|
101
|
+
export let getServiceErrorData = (error: unknown): SlateServiceErrorData | null => {
|
|
102
|
+
if (isServiceError(error)) {
|
|
103
|
+
return isServiceErrorData(error.data) ? error.data : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isServiceErrorData(error)) {
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isStructuredValue(error) && 'data' in error && isServiceErrorData(error.data)) {
|
|
111
|
+
return error.data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isStructuredValue(error) && 'toResponse' in error && typeof error.toResponse === 'function') {
|
|
115
|
+
let response = error.toResponse();
|
|
116
|
+
return isServiceErrorData(response) ? response : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export let mapServiceErrorToSlateErrorInput = (
|
|
123
|
+
error:
|
|
124
|
+
| ServiceError<ErrorRecord<any, any>>
|
|
125
|
+
| ErrorRecord<any, any>
|
|
126
|
+
| ErrorData<any, any>
|
|
127
|
+
| SlateServiceErrorData,
|
|
128
|
+
defaults: Partial<SlateErrorInput> = {}
|
|
129
|
+
): SlateErrorInput => {
|
|
130
|
+
let data = getServiceErrorData(error);
|
|
131
|
+
if (!data) {
|
|
132
|
+
return {
|
|
133
|
+
code: defaults.code ?? DEFAULT_CODE,
|
|
134
|
+
message: defaults.message ?? 'An unexpected internal error occurred.',
|
|
135
|
+
kind: defaults.kind,
|
|
136
|
+
retryable: defaults.retryable,
|
|
137
|
+
status: defaults.status,
|
|
138
|
+
provider: defaults.provider,
|
|
139
|
+
upstream: defaults.upstream,
|
|
140
|
+
baggage: defaults.baggage
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let code = defaults.code ?? mapServiceErrorCode(data.code, data.status);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
code,
|
|
148
|
+
message: defaults.message ?? data.message,
|
|
149
|
+
kind: defaults.kind ?? normalizeKind(code),
|
|
150
|
+
retryable: defaults.retryable ?? normalizeRetryable(code),
|
|
151
|
+
status: defaults.status ?? data.status,
|
|
152
|
+
issues: defaults.issues ?? normalizeServiceErrorIssues(data.errors),
|
|
153
|
+
provider: defaults.provider,
|
|
154
|
+
upstream: defaults.upstream,
|
|
155
|
+
baggage: cleanRecord({
|
|
156
|
+
...(defaults.baggage ?? {}),
|
|
157
|
+
...getServiceErrorBaggage(data)
|
|
158
|
+
})
|
|
159
|
+
};
|
|
160
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AxiosError, AxiosResponse } from 'axios';
|
|
2
|
+
import type { SlateError } from './base';
|
|
3
|
+
|
|
4
|
+
export type SlateErrorKind =
|
|
5
|
+
| 'declaration'
|
|
6
|
+
| 'validation'
|
|
7
|
+
| 'request'
|
|
8
|
+
| 'auth'
|
|
9
|
+
| 'config'
|
|
10
|
+
| 'resource'
|
|
11
|
+
| 'payment'
|
|
12
|
+
| 'upstream'
|
|
13
|
+
| 'transport'
|
|
14
|
+
| 'internal';
|
|
15
|
+
|
|
16
|
+
export interface SlateErrorIssue {
|
|
17
|
+
path?: string[];
|
|
18
|
+
code?: string;
|
|
19
|
+
message: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SlateErrorProviderInfo {
|
|
24
|
+
key?: string;
|
|
25
|
+
service?: string;
|
|
26
|
+
operation?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SlateErrorUpstreamInfo {
|
|
30
|
+
status?: number;
|
|
31
|
+
code?: string;
|
|
32
|
+
type?: string;
|
|
33
|
+
requestId?: string;
|
|
34
|
+
method?: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SlateErrorResponse {
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
kind: SlateErrorKind;
|
|
42
|
+
retryable?: boolean;
|
|
43
|
+
status?: number;
|
|
44
|
+
issues?: SlateErrorIssue[];
|
|
45
|
+
provider?: SlateErrorProviderInfo;
|
|
46
|
+
upstream?: SlateErrorUpstreamInfo;
|
|
47
|
+
baggage?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SlateErrorInput extends Omit<SlateErrorResponse, 'kind'> {
|
|
51
|
+
kind?: SlateErrorKind;
|
|
52
|
+
cause?: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SlateAxiosErrorOptions {
|
|
56
|
+
defaults?: Partial<SlateErrorResponse>;
|
|
57
|
+
extractResponseData?: (response: AxiosResponse) => unknown;
|
|
58
|
+
mapAxiosError?: (
|
|
59
|
+
error: AxiosError,
|
|
60
|
+
inferred: SlateErrorResponse
|
|
61
|
+
) => SlateError | Partial<SlateErrorResponse> | void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SlateServiceErrorData {
|
|
65
|
+
status: number;
|
|
66
|
+
code: string;
|
|
67
|
+
message: string;
|
|
68
|
+
hint?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
reason?: string;
|
|
71
|
+
errors?: unknown;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { DEFAULT_CODE, DEFAULT_MESSAGES } from './defaults';
|
|
2
|
+
import type {
|
|
3
|
+
SlateErrorInput,
|
|
4
|
+
SlateErrorKind,
|
|
5
|
+
SlateErrorProviderInfo,
|
|
6
|
+
SlateErrorResponse,
|
|
7
|
+
SlateErrorUpstreamInfo
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
export let isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
11
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
|
|
13
|
+
export let normalizeKind = (code: string, kind?: SlateErrorKind): SlateErrorKind => {
|
|
14
|
+
if (kind) return kind;
|
|
15
|
+
if (code.startsWith('declaration.')) return 'declaration';
|
|
16
|
+
if (code.startsWith('input.')) return 'validation';
|
|
17
|
+
if (code.startsWith('request.')) return 'request';
|
|
18
|
+
if (code.startsWith('config.')) return 'config';
|
|
19
|
+
if (code.startsWith('auth.') || code.startsWith('permission.')) return 'auth';
|
|
20
|
+
if (code.startsWith('resource.')) return 'resource';
|
|
21
|
+
if (code.startsWith('payment.')) return 'payment';
|
|
22
|
+
if (code.startsWith('transport.')) return 'transport';
|
|
23
|
+
if (code.startsWith('upstream.')) return 'upstream';
|
|
24
|
+
return 'internal';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export let normalizeStatus = (code: string, status?: number) => {
|
|
28
|
+
if (status) return status;
|
|
29
|
+
if (code === 'input.invalid' || code === 'request.bad' || code === 'config.invalid')
|
|
30
|
+
return 400;
|
|
31
|
+
if (code === 'request.invalid_version') return 400;
|
|
32
|
+
if (code === 'auth.required' || code === 'auth.invalid' || code === 'auth.expired')
|
|
33
|
+
return 401;
|
|
34
|
+
if (code === 'payment.required') return 402;
|
|
35
|
+
if (code === 'permission.denied') return 403;
|
|
36
|
+
if (code === 'resource.not_found') return 404;
|
|
37
|
+
if (code === 'resource.conflict') return 409;
|
|
38
|
+
if (code === 'resource.gone') return 410;
|
|
39
|
+
if (code === 'request.not_acceptable') return 406;
|
|
40
|
+
if (code === 'request.precondition_failed') return 412;
|
|
41
|
+
if (code === 'request.rate_limited' || code === 'upstream.rate_limited') return 429;
|
|
42
|
+
if (code === 'operation.not_implemented') return 501;
|
|
43
|
+
if (code === 'upstream.invalid_request') return 400;
|
|
44
|
+
if (code === 'upstream.timeout' || code === 'internal.timeout') return 504;
|
|
45
|
+
if (code === 'upstream.unavailable') return 503;
|
|
46
|
+
if (code === 'upstream.error') return 502;
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export let normalizeRetryable = (code: string, retryable?: boolean) => {
|
|
51
|
+
if (typeof retryable === 'boolean') return retryable;
|
|
52
|
+
return (
|
|
53
|
+
code === 'request.rate_limited' ||
|
|
54
|
+
code === 'upstream.rate_limited' ||
|
|
55
|
+
code === 'upstream.timeout' ||
|
|
56
|
+
code === 'upstream.network_error' ||
|
|
57
|
+
code === 'upstream.unavailable' ||
|
|
58
|
+
code === 'upstream.error' ||
|
|
59
|
+
code === 'transport.invoke_failed' ||
|
|
60
|
+
code === 'internal.timeout'
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export let getDefaultMessage = (code: string, message?: string) => {
|
|
65
|
+
if (message && message.trim()) return message;
|
|
66
|
+
return DEFAULT_MESSAGES[code] ?? DEFAULT_MESSAGES[DEFAULT_CODE];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export let cleanRecord = <Value extends Record<string, unknown> | undefined>(value: Value) => {
|
|
70
|
+
if (!value) return undefined;
|
|
71
|
+
|
|
72
|
+
let entries = Object.entries(value).filter(([, entry]) => entry !== undefined);
|
|
73
|
+
if (entries.length === 0) return undefined;
|
|
74
|
+
return Object.fromEntries(entries) as Value;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export let mergeRecords = (
|
|
78
|
+
left?: Record<string, unknown>,
|
|
79
|
+
right?: Record<string, unknown>
|
|
80
|
+
): Record<string, unknown> | undefined => {
|
|
81
|
+
if (!left && !right) return undefined;
|
|
82
|
+
return cleanRecord({
|
|
83
|
+
...(left ?? {}),
|
|
84
|
+
...(right ?? {})
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export let mergeSlateErrorData = (
|
|
89
|
+
base: SlateErrorResponse,
|
|
90
|
+
override?: Partial<SlateErrorResponse>
|
|
91
|
+
): SlateErrorResponse => {
|
|
92
|
+
if (!override) return base;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
...override,
|
|
97
|
+
provider: cleanRecord({
|
|
98
|
+
...(base.provider ?? {}),
|
|
99
|
+
...(override.provider ?? {})
|
|
100
|
+
}) as SlateErrorProviderInfo | undefined,
|
|
101
|
+
upstream: cleanRecord({
|
|
102
|
+
...(base.upstream ?? {}),
|
|
103
|
+
...(override.upstream ?? {})
|
|
104
|
+
}) as SlateErrorUpstreamInfo | undefined,
|
|
105
|
+
baggage: mergeRecords(base.baggage, override.baggage),
|
|
106
|
+
issues: override.issues ?? base.issues
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export let sanitizeForBaggage = (value: unknown, depth = 0): unknown => {
|
|
111
|
+
if (value == null) return value;
|
|
112
|
+
if (depth >= 4) return '[truncated]';
|
|
113
|
+
if (typeof value === 'string') {
|
|
114
|
+
return value.length > 2_000 ? `${value.slice(0, 2_000)}...[truncated]` : value;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
return value.slice(0, 20).map(item => sanitizeForBaggage(item, depth + 1));
|
|
121
|
+
}
|
|
122
|
+
if (isRecord(value)) {
|
|
123
|
+
return Object.fromEntries(
|
|
124
|
+
Object.entries(value)
|
|
125
|
+
.slice(0, 30)
|
|
126
|
+
.map(([key, entry]) => [key, sanitizeForBaggage(entry, depth + 1)])
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return String(value);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export let normalizeSlateErrorInput = (input: SlateErrorInput): SlateErrorResponse => {
|
|
133
|
+
let code = input.code || DEFAULT_CODE;
|
|
134
|
+
return {
|
|
135
|
+
code,
|
|
136
|
+
message: getDefaultMessage(code, input.message),
|
|
137
|
+
kind: normalizeKind(code, input.kind),
|
|
138
|
+
retryable: normalizeRetryable(code, input.retryable),
|
|
139
|
+
status: normalizeStatus(code, input.status ?? input.upstream?.status),
|
|
140
|
+
issues: input.issues,
|
|
141
|
+
provider: cleanRecord(input.provider as Record<string, unknown> | undefined) as
|
|
142
|
+
| SlateErrorProviderInfo
|
|
143
|
+
| undefined,
|
|
144
|
+
upstream: cleanRecord({
|
|
145
|
+
...input.upstream,
|
|
146
|
+
status: input.upstream?.status ?? input.status
|
|
147
|
+
}) as SlateErrorUpstreamInfo | undefined,
|
|
148
|
+
baggage: cleanRecord(input.baggage)
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export let isSlateErrorResponse = (error: unknown): error is SlateErrorResponse => {
|
|
153
|
+
if (!isRecord(error)) return false;
|
|
154
|
+
return typeof error.code === 'string' && typeof error.message === 'string';
|
|
155
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { SlateLogger, type SlateLogEntry } from './logger';
|
|
3
|
+
|
|
4
|
+
let flushLogs = async () => {
|
|
5
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
describe('SlateLogger', () => {
|
|
9
|
+
it('keeps a readable message and structured data for object logs', async () => {
|
|
10
|
+
let entries: SlateLogEntry[] = [];
|
|
11
|
+
let logger = new SlateLogger([
|
|
12
|
+
batch => {
|
|
13
|
+
entries.push(...batch);
|
|
14
|
+
}
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
logger.info({
|
|
18
|
+
message: 'Invoking tool action',
|
|
19
|
+
providerId: 'demo-slate',
|
|
20
|
+
actionId: 'echo',
|
|
21
|
+
phase: 'start'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await flushLogs();
|
|
25
|
+
|
|
26
|
+
expect(entries[0]).toMatchObject({
|
|
27
|
+
type: 'info',
|
|
28
|
+
message: 'Invoking tool action',
|
|
29
|
+
data: {
|
|
30
|
+
providerId: 'demo-slate',
|
|
31
|
+
actionId: 'echo',
|
|
32
|
+
phase: 'start'
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('captures error details as structured metadata', async () => {
|
|
38
|
+
let entries: SlateLogEntry[] = [];
|
|
39
|
+
let logger = new SlateLogger([
|
|
40
|
+
batch => {
|
|
41
|
+
entries.push(...batch);
|
|
42
|
+
}
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
logger.error(new Error('boom'));
|
|
46
|
+
|
|
47
|
+
await flushLogs();
|
|
48
|
+
|
|
49
|
+
expect(entries[0]?.message).toBe('Error: boom');
|
|
50
|
+
expect(entries[0]?.data).toMatchObject({
|
|
51
|
+
error: {
|
|
52
|
+
name: 'Error',
|
|
53
|
+
message: 'boom'
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/logger/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { encode } from '@toon-format/toon';
|
|
2
2
|
|
|
3
3
|
export type SlateLogType = 'info' | 'warn' | 'error' | 'progress';
|
|
4
|
+
export type SlateLogFields = Record<string, unknown>;
|
|
4
5
|
|
|
5
6
|
export type SlateLogMessageInput =
|
|
6
7
|
| string
|
|
@@ -12,22 +13,79 @@ export type SlateLogMessageInput =
|
|
|
12
13
|
| object
|
|
13
14
|
| SlateLogMessageInput[];
|
|
14
15
|
|
|
15
|
-
let
|
|
16
|
+
let isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
17
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
|
|
19
|
+
let normalizeMessage = (
|
|
20
|
+
message: SlateLogMessageInput
|
|
21
|
+
): {
|
|
22
|
+
message: string;
|
|
23
|
+
data?: SlateLogFields;
|
|
24
|
+
} => {
|
|
25
|
+
if (Array.isArray(message)) {
|
|
26
|
+
let parts = message.map(part => normalizeMessage(part));
|
|
27
|
+
let combinedMessage = parts
|
|
28
|
+
.map(part => part.message)
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(' ')
|
|
31
|
+
.trim();
|
|
32
|
+
|
|
33
|
+
let dataParts = parts
|
|
34
|
+
.map(part => {
|
|
35
|
+
if (part.data) {
|
|
36
|
+
return {
|
|
37
|
+
message: part.message,
|
|
38
|
+
...part.data
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return part.message;
|
|
43
|
+
})
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
message: combinedMessage || 'Log entry',
|
|
48
|
+
data: dataParts.length > 0 ? { parts: dataParts } : undefined
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
if (typeof message === 'object') {
|
|
17
|
-
if (message === null) return 'null';
|
|
53
|
+
if (message === null) return { message: 'null' };
|
|
18
54
|
if (message instanceof Error) {
|
|
19
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
message: `${message.name}: ${message.message}`,
|
|
57
|
+
data: {
|
|
58
|
+
error: {
|
|
59
|
+
name: message.name,
|
|
60
|
+
message: message.message,
|
|
61
|
+
stack: message.stack
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
20
65
|
}
|
|
21
|
-
|
|
66
|
+
|
|
67
|
+
if (isRecord(message) && typeof message.message === 'string') {
|
|
68
|
+
let { message: text, ...data } = message;
|
|
69
|
+
return {
|
|
70
|
+
message: text || 'Log entry',
|
|
71
|
+
data: Object.keys(data).length > 0 ? data : undefined
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
message: encode(message),
|
|
77
|
+
data: isRecord(message) ? message : undefined
|
|
78
|
+
};
|
|
22
79
|
}
|
|
23
80
|
|
|
24
|
-
return String(message);
|
|
81
|
+
return { message: String(message) };
|
|
25
82
|
};
|
|
26
83
|
|
|
27
84
|
export interface SlateLogEntry {
|
|
28
85
|
type: SlateLogType;
|
|
29
86
|
message: string;
|
|
30
87
|
timestamp: Date;
|
|
88
|
+
data?: SlateLogFields;
|
|
31
89
|
}
|
|
32
90
|
|
|
33
91
|
export type SlateLogListener = (entries: SlateLogEntry[]) => void;
|
|
@@ -44,9 +102,10 @@ export class SlateLogger {
|
|
|
44
102
|
private flush() {
|
|
45
103
|
if (this.#timer) clearTimeout(this.#timer);
|
|
46
104
|
if (this.#logs.length === 0) return;
|
|
105
|
+
let logs = this.#logs.splice(0);
|
|
47
106
|
|
|
48
107
|
for (let listener of this.#listeners) {
|
|
49
|
-
listener(
|
|
108
|
+
listener(logs);
|
|
50
109
|
}
|
|
51
110
|
}
|
|
52
111
|
|
|
@@ -59,27 +118,29 @@ export class SlateLogger {
|
|
|
59
118
|
}
|
|
60
119
|
|
|
61
120
|
log(type: SlateLogType, message: SlateLogMessageInput) {
|
|
121
|
+
let normalized = normalizeMessage(message);
|
|
62
122
|
this.#logs.push({
|
|
63
123
|
type,
|
|
64
124
|
timestamp: new Date(),
|
|
65
|
-
message:
|
|
125
|
+
message: normalized.message,
|
|
126
|
+
data: normalized.data
|
|
66
127
|
});
|
|
67
128
|
this.scheduleFlush();
|
|
68
129
|
}
|
|
69
130
|
|
|
70
131
|
info(...message: SlateLogMessageInput[]) {
|
|
71
|
-
this.log('info', message.flat());
|
|
132
|
+
this.log('info', message.length === 1 ? message[0]! : message.flat());
|
|
72
133
|
}
|
|
73
134
|
|
|
74
135
|
warn(...message: SlateLogMessageInput[]) {
|
|
75
|
-
this.log('warn', message.flat());
|
|
136
|
+
this.log('warn', message.length === 1 ? message[0]! : message.flat());
|
|
76
137
|
}
|
|
77
138
|
|
|
78
139
|
error(...message: SlateLogMessageInput[]) {
|
|
79
|
-
this.log('error', message.flat());
|
|
140
|
+
this.log('error', message.length === 1 ? message[0]! : message.flat());
|
|
80
141
|
}
|
|
81
142
|
|
|
82
143
|
progress(...message: SlateLogMessageInput[]) {
|
|
83
|
-
this.log('progress', message.flat());
|
|
144
|
+
this.log('progress', message.length === 1 ? message[0]! : message.flat());
|
|
84
145
|
}
|
|
85
146
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SlateTool, SlateTrigger } from '../action';
|
|
2
|
-
import { SlateSpecification } from './specification';
|
|
1
|
+
import type { SlateTool, SlateTrigger } from '../action';
|
|
2
|
+
import type { SlateSpecification } from './specification';
|
|
3
3
|
|
|
4
4
|
export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
5
5
|
private constructor(
|