@wdyy/skills 0.1.4 → 0.1.5
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/.well-known/skills/index.json +2 -2
- package/.well-known/skills/wdyy-api-standard/SKILL.md +3 -2
- package/.well-known/skills/wdyy-api-standard/reference/{api-/346/240/271/346/215/256/345/260/261/350/257/212/345/217/267/350/216/267/345/217/226/346/243/200/351/252/214/346/225/260/346/215/256.md → api-demo.md} +5 -3
- package/.well-known/skills/wdyy-api-standard/reference/api-sms.md +67 -0
- package/.well-known/skills/wdyy-logging-standard/SKILL.md +16 -20
- package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +6 -6
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.mjs +0 -19
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +22 -25
- package/.well-known/skills/wdyy-logging-standard/templates/frontend-error-report.template.ts +1 -6
- package/.well-known/skills/wdyy-logging-standard/templates/logger.template.ts +20 -26
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"name": "wdyy-api-standard",
|
|
15
15
|
"description": "根据“获取XX数据”“使用XX接口”等具体需求,定位已登记的接口 reference,并按已确认的接口契约将调用方式写入程序。Use when 需要查询或实现一个已登记接口的数据调用时。",
|
|
16
|
-
"files": ["SKILL.md", "agents/openai.yaml", "reference/api
|
|
16
|
+
"files": ["SKILL.md", "agents/openai.yaml", "reference/api-demo.md", "reference/api-sms.md"]
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "wdyy-bug-record",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "wdyy-logging-standard",
|
|
25
|
-
"description": "为 NestJS 和 Vue 项目实现前端异常上报、后端统一结构化 JSON
|
|
25
|
+
"description": "为 NestJS 和 Vue 项目实现前端异常上报、后端统一结构化 JSON 日志、traceId 传递、完整原始入参与 2MB 文件轮转。Use when 编写日志、错误处理、接口日志或审查生产可观测性时。",
|
|
26
26
|
"files": ["SKILL.md", "agents/openai.yaml", "reference/logging-rules.md", "scripts/validate-log-entry.mjs", "scripts/validate-log-entry.test.mjs", "templates/frontend-error-report.template.ts", "templates/logger.template.ts"]
|
|
27
27
|
}
|
|
28
28
|
]
|
|
@@ -16,16 +16,17 @@ description: 根据“获取XX数据”“使用XX接口”等具体需求,定
|
|
|
16
16
|
|
|
17
17
|
## 路由步骤
|
|
18
18
|
|
|
19
|
-
1. 根据需求中的数据名称、业务对象或接口名称,在 `reference/api-*.md`
|
|
19
|
+
1. 根据需求中的数据名称、业务对象或接口名称,在 `reference/api-*.md` 中查找候选业务接口,排除仅用于说明格式的 `api-demo.md`。
|
|
20
20
|
2. 仅在唯一匹配时读取该 reference,使用其中的接口 URL、JSON 入参和 JSON 返回值编写调用方式。
|
|
21
21
|
3. 按 reference 的“备注”实现特殊调用要求。
|
|
22
22
|
4. 若没有匹配接口、存在多个候选接口,或 URL、入参、返回值不完整,先要求补充或确认接口信息;不得编造调用细节。
|
|
23
23
|
|
|
24
24
|
## 接口文档约定
|
|
25
25
|
|
|
26
|
+
- `reference/api-demo.md` 仅说明接口文档格式,不得作为业务调用依据;其中可使用 `XXX` 等占位内容。
|
|
26
27
|
- 一个接口对应一个 `reference/api-接口名称.md` 文件。
|
|
27
28
|
- 文件名去除 `.md` 后必须与“接口名称”字段一致。
|
|
28
|
-
-
|
|
29
|
+
- 每个业务接口文档必须按以下顺序包含六个一级标题:
|
|
29
30
|
|
|
30
31
|
```md
|
|
31
32
|
# 接口名称:api-xxx
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 接口名称:api-sms
|
|
2
|
+
|
|
3
|
+
# 接口用途:向一个或多个手机号发送相同的短信内容
|
|
4
|
+
|
|
5
|
+
# 接口URL:http://172.26.1.80:8998/api/sms/send
|
|
6
|
+
|
|
7
|
+
`POST` 请求,`Content-Type` 为 `application/json; charset=utf-8`。当前地址为内网默认地址;生产或测试环境地址如有差异,以部署方提供的地址为准。
|
|
8
|
+
|
|
9
|
+
# 入参格式:JSON
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mobiles": [
|
|
14
|
+
"13800000001",
|
|
15
|
+
"13800000002"
|
|
16
|
+
],
|
|
17
|
+
"content": "【系统通知】您的验证码是1234"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `mobiles`:必填,`array[string]`,接收短信的手机号列表;单次最多 200 个手机号。
|
|
22
|
+
- `content`:必填,`string`,短信正文内容。
|
|
23
|
+
- 接入方不传入 token、`userCode` 或 `token` 等短信网关凭证。
|
|
24
|
+
|
|
25
|
+
# 返回值:JSON
|
|
26
|
+
|
|
27
|
+
成功提交至短信网关时返回 HTTP `200`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"msgId": "2601291081805244415"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`msgId` 为短信网关消息流水号,表示请求已提交至网关,不表示短信已最终送达。
|
|
36
|
+
|
|
37
|
+
失败或被拦截时返回 `error` 字段。手机号列表为空或短信内容为空时返回 HTTP `400`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"error": "Error: Mobile number array cannot be empty."
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"error": "Error: Content cannot be empty."
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
触发发送限流时返回 HTTP `429`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"error": "Too many requests: 手机号138****0001获取短信过于频繁,请稍后再试"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
短信网关拒绝或返回异常时,接口仍可能返回 HTTP `200`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"error": "-41"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
# 备注:使用 `POST` 请求调用;响应体存在 `msgId` 才表示提交成功,存在 `error` 表示发送失败或被拦截,不能只根据 HTTP 状态码判断成功。调用方应避免在日志中完整输出手机号、短信内容及验证码。调用前确认短信签名与模板已报备,并遵守单次最多 200 个手机号、单 IP 每分钟最多 120 次、单手机号每分钟最多 3 次的限流规则。
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wdyy-logging-standard
|
|
3
|
-
description: 为 NestJS 和 Vue 项目实现前端异常上报、后端统一结构化 JSON
|
|
3
|
+
description: 为 NestJS 和 Vue 项目实现前端异常上报、后端统一结构化 JSON 日志、traceId 传递、完整原始入参与 2MB 文件轮转。Use when 编写日志、错误处理、接口日志或审查生产可观测性时。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 企业日志规范
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
建立可追踪、可轮转的结构化日志,以支撑运行时排障并保留完整原始上下文。
|
|
11
11
|
|
|
12
12
|
## When to Use
|
|
13
13
|
|
|
@@ -15,51 +15,47 @@ description: 为 NestJS 和 Vue 项目实现前端异常上报、后端统一结
|
|
|
15
15
|
|
|
16
16
|
## 协作边界
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
日志实现必须纳入可观测性、实现计划和测试验证,不能降低 RED 指标、追踪或告警要求。
|
|
19
19
|
|
|
20
20
|
## 输入与输出
|
|
21
21
|
|
|
22
|
-
-
|
|
23
|
-
- 输出:统一 logger、前端异常上报客户端、HTTP
|
|
22
|
+
- 输入:服务名、实例标识、环境、请求上下文、`LOG_DIR`。
|
|
23
|
+
- 输出:统一 logger、前端异常上报客户端、HTTP 日志中间件、日志文件命名规则和日志验证结果。
|
|
24
24
|
- 使用 [日志规则](reference/logging-rules.md) 和 [logger 模板](templates/logger.template.ts)。
|
|
25
25
|
|
|
26
26
|
## 执行步骤
|
|
27
27
|
|
|
28
|
-
1. 定义 `timestamp`、`level`、`service`、`instanceId`、`env`、`traceId`、`userId`、`method`、`path`、`query`、`body`、`statusCode`、`result`、`durationMs`、`errorCode`、`message`、`params` 的最小 JSON 契约。`timestamp` 必须为服务器本地时间 `YYYY-MM-DD HH:mm:ss`,精确到秒。
|
|
28
|
+
1. 定义 `timestamp`、`level`、`service`、`instanceId`、`env`、`traceId`、`userId`、`method`、`path`、`query`、`body`、`response`、`statusCode`、`result`、`durationMs`、`errorCode`、`message`、`params` 的最小 JSON 契约。`timestamp` 必须为服务器本地时间 `YYYY-MM-DD HH:mm:ss`,精确到秒。
|
|
29
29
|
2. 在入口生成或透传 traceId;在内部 REST 调用中继续传递。
|
|
30
30
|
3. 统一封装 logger;业务代码不得直接使用 `console.log`。
|
|
31
|
-
4. HTTP
|
|
32
|
-
5.
|
|
33
|
-
6. Vue 捕获未处理异常和 Promise
|
|
34
|
-
7.
|
|
31
|
+
4. HTTP 请求日志记录完整的原始 `query`(查询参数)和 `body`(请求体);授权请求可记录实际用户、模式、表和权限等入参。服务端返回成功时记录 `result: success`;返回失败时记录 `result: failure`,并在 `response` 中记录完整失败内容。HTTP `statusCode` 为 100–399 时为成功,400–599 时为失败。
|
|
32
|
+
5. 日志不执行脱敏、数组截断或大对象裁剪;`body` 必须保留完整原始入参。
|
|
33
|
+
6. Vue 捕获未处理异常和 Promise 拒绝并上报后端;浏览器不得尝试写服务器文件。
|
|
34
|
+
7. 后端直接按 `LOG_DIR/yyyy-mm-dd_hh24-mm-ss.log` 写入,禁止创建子目录。若同一秒需要创建多个文件,依次使用 `yyyy-mm-dd_hh24-mm-ss_1.log`、`yyyy-mm-dd_hh24-mm-ss_2.log` 等递增后缀;既有日志文件不得重命名。最新文件超过 2MB 时创建新文件,并测试轮转。
|
|
35
35
|
|
|
36
36
|
## 禁止事项
|
|
37
37
|
|
|
38
|
-
- 不得记录秘密、完整敏感身份信息或未裁剪的大请求/响应对象。
|
|
39
38
|
- 不得将 `message` 仅限制为 `success` 或 `error`。
|
|
40
39
|
- 不得用 console 输出替代业务日志。
|
|
41
40
|
|
|
42
41
|
## Red Flags
|
|
43
42
|
|
|
44
|
-
- 业务代码直接调用 console
|
|
43
|
+
- 业务代码直接调用 console,或 `body` 未保留完整原始入参。
|
|
45
44
|
- 请求日志缺失 traceId、状态码或耗时。
|
|
46
|
-
-
|
|
45
|
+
- 同秒创建的日志文件覆盖既有文件,或前端异常未进入统一后端日志。
|
|
47
46
|
|
|
48
47
|
## Verification
|
|
49
48
|
|
|
50
49
|
- [ ] 正常、异常和 HTTP 请求日志均为可解析 JSON。
|
|
51
50
|
- [ ] traceId 跨入口与内部调用可追踪。
|
|
52
|
-
- [ ]
|
|
53
|
-
- [ ]
|
|
54
|
-
- [ ]
|
|
51
|
+
- [ ] 完整 HTTP 入参、服务端成功/失败结果映射、失败响应内容与同秒递增命名均有测试覆盖,轮转阈值可验证。
|
|
52
|
+
- [ ] 前端异常上报经过后端身份校验。
|
|
53
|
+
- [ ] 日志直接写入持久化挂载的 `LOG_DIR`,容器替换后日志仍保留。
|
|
55
54
|
- [ ] `LOG_DIR` 缺失或不可写时服务以明确错误停止。
|
|
56
55
|
|
|
57
|
-
无法确定某字段是否敏感时按敏感处理并要求数据负责人确认;不得先原样记录再补救。
|
|
58
|
-
|
|
59
56
|
## Common Rationalizations
|
|
60
57
|
|
|
61
58
|
| 合理化说法 | 事实 |
|
|
62
59
|
|---|---|
|
|
63
|
-
| “开发日志不用脱敏” | 开发日志同样会被共享和长期保留。 |
|
|
64
60
|
| “console.log 足够定位问题” | 无统一字段、轮转和 traceId 的输出不能支撑运维。 |
|
|
65
|
-
|
|
|
61
|
+
| “失败响应只记录状态码即可” | 必须记录完整失败内容,才能定位服务端返回的业务错误。 |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Enterprise Logging Standard"
|
|
3
|
-
short_description: "Implement
|
|
4
|
-
default_prompt: "Use $wdyy-logging-standard to implement
|
|
3
|
+
short_description: "Implement full-context structured logs and frontend error reporting"
|
|
4
|
+
default_prompt: "Use $wdyy-logging-standard to implement full-context structured logs, result recording, and frontend error reporting."
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# 日志规则
|
|
2
2
|
|
|
3
3
|
- 日志为一行 JSON,最小字段见 `logger.template.ts`。
|
|
4
|
+
- `logger.template.ts` 必须可在未安装 `@types/node` 的项目中独立类型检查;读取环境变量时通过 `globalThis` 声明最小运行时形状,不直接依赖 Node.js 全局类型。
|
|
4
5
|
- `timestamp` 使用服务器本地时间 `YYYY-MM-DD HH:mm:ss`,精确到秒;不得包含毫秒、`T` 或 `Z`。
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
- HTTP 日志必须保留 `statusCode
|
|
8
|
-
- password、token、secret、authorization、databaseUrl、idCard、bankCard 必须从输出中完全移除,不得以占位符输出。数组保留前 500 项;超出时追加 `[TRUNCATED N ITEMS]`。
|
|
6
|
+
- 文件直接写入 `LOG_DIR`,不得创建服务、实例或日期子目录。文件名使用服务器本地时间 `yyyy-mm-dd_hh24-mm-ss.log`;同一秒内创建多个文件时,后续文件依次为 `yyyy-mm-dd_hh24-mm-ss_1.log`、`yyyy-mm-dd_hh24-mm-ss_2.log`。最新日志文件大于 2MB 时先创建新文件再写入,既有日志文件不得重命名或覆盖。
|
|
7
|
+
- 日志仅记录关键业务事件、请求和异常;`params`、`query`、`body` 和 `response` 均保留完整原始值,不执行脱敏、数组截断或大对象裁剪。授权请求可记录实际用户、模式、表和权限等入参。
|
|
8
|
+
- HTTP 日志必须保留 `statusCode`。服务端成功返回时 `result` 为 `success`;失败返回时 `result` 为 `failure`,并在 `response` 中保留完整失败内容。100–399 为成功,400–599 为失败。
|
|
9
9
|
- HTTP 入口和内部服务调用必须透传 traceId。
|
|
10
|
-
- 每条日志必须包含 `instanceId
|
|
11
|
-
- Vue 未处理异常和 Promise
|
|
10
|
+
- 每条日志必须包含 `instanceId`;通过同秒递增后缀避免多个实例或进程覆盖同一日志文件。
|
|
11
|
+
- Vue 未处理异常和 Promise 拒绝必须上报后端;后端校验请求身份后写入统一日志。
|
|
12
12
|
- 日志目录必须挂载到容器外部持久路径,替换容器不得删除历史日志。
|
|
@@ -9,25 +9,6 @@ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(entry.timestamp)) {
|
|
|
9
9
|
throw new Error('timestamp must use local YYYY-MM-DD HH:mm:ss format');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const sensitiveKeys = new Set([
|
|
13
|
-
'password', 'token', 'secret', 'authorization', 'databaseurl', 'idcard', 'bankcard',
|
|
14
|
-
]);
|
|
15
|
-
const validateValue = (value) => {
|
|
16
|
-
if (Array.isArray(value)) {
|
|
17
|
-
if (value.length > 500 && (value.length !== 501 || !/^\[TRUNCATED \d+ ITEMS\]$/.test(value.at(-1)))) {
|
|
18
|
-
throw new Error('Arrays over 500 items must end with a truncation marker');
|
|
19
|
-
}
|
|
20
|
-
value.forEach(validateValue);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (!value || typeof value !== 'object') return;
|
|
24
|
-
for (const [key, item] of Object.entries(value)) {
|
|
25
|
-
if (sensitiveKeys.has(key.toLowerCase())) throw new Error('Sensitive fields must be omitted');
|
|
26
|
-
validateValue(item);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
validateValue(entry);
|
|
30
|
-
|
|
31
12
|
if ('statusCode' in entry) {
|
|
32
13
|
const expected = entry.statusCode >= 100 && entry.statusCode <= 399 ? 'success'
|
|
33
14
|
: entry.statusCode >= 400 && entry.statusCode <= 599 ? 'failure' : undefined;
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { afterEach, test } from 'node:test';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createLogFileName } from '../templates/logger.template.ts';
|
|
7
8
|
|
|
8
9
|
const validator = new URL('./validate-log-entry.mjs', import.meta.url);
|
|
9
10
|
const temporaryDirectories = [];
|
|
@@ -24,6 +25,15 @@ async function validate(entry) {
|
|
|
24
25
|
return spawnSync(process.execPath, [validator.pathname, input], { encoding: 'utf8' });
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
test('同一秒日志文件使用递增后缀', () => {
|
|
29
|
+
const date = new Date(2026, 6, 28, 9, 5, 3);
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
[createLogFileName(date), createLogFileName(date, 1), createLogFileName(date, 2)],
|
|
33
|
+
['2026-07-28_09-05-03.log', '2026-07-28_09-05-03_1.log', '2026-07-28_09-05-03_2.log'],
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
27
37
|
test('蓝绿实例日志必须包含 instanceId', async () => {
|
|
28
38
|
const result = await validate({
|
|
29
39
|
timestamp: '2026-07-09 08:00:00',
|
|
@@ -66,24 +76,7 @@ test('HTTP 状态码与 result 不一致时明确失败', async () => {
|
|
|
66
76
|
assert.match(result.stderr, /result/);
|
|
67
77
|
});
|
|
68
78
|
|
|
69
|
-
test('
|
|
70
|
-
for (const key of ['password', 'token', 'secret', 'authorization', 'databaseUrl', 'idCard', 'bankCard']) {
|
|
71
|
-
const result = await validate({
|
|
72
|
-
timestamp: '2026-07-09 08:00:00',
|
|
73
|
-
level: 'info',
|
|
74
|
-
service: 'backend',
|
|
75
|
-
instanceId: 'blue',
|
|
76
|
-
env: 'production',
|
|
77
|
-
message: 'request completed',
|
|
78
|
-
body: { [key]: 'not-allowed' },
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
assert.notEqual(result.status, 0, key);
|
|
82
|
-
assert.match(result.stderr, /Sensitive/, key);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('HTTP 成功结果与截断数组可通过验证', async () => {
|
|
79
|
+
test('完整原始入参和成功响应可通过验证', async () => {
|
|
87
80
|
const result = await validate({
|
|
88
81
|
timestamp: '2026-07-09 08:00:00',
|
|
89
82
|
level: 'info',
|
|
@@ -93,9 +86,11 @@ test('HTTP 成功结果与截断数组可通过验证', async () => {
|
|
|
93
86
|
message: 'request completed',
|
|
94
87
|
query: { page: '1' },
|
|
95
88
|
body: {
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
password: 'unredacted',
|
|
90
|
+
idCard: '110101199001011234',
|
|
91
|
+
permissions: Array.from({ length: 501 }, (_, index) => index),
|
|
98
92
|
},
|
|
93
|
+
response: { code: 'OK', message: '处理成功' },
|
|
99
94
|
statusCode: 201,
|
|
100
95
|
result: 'success',
|
|
101
96
|
});
|
|
@@ -103,19 +98,21 @@ test('HTTP 成功结果与截断数组可通过验证', async () => {
|
|
|
103
98
|
assert.equal(result.status, 0, result.stderr);
|
|
104
99
|
});
|
|
105
100
|
|
|
106
|
-
test('
|
|
101
|
+
test('失败响应内容和 failure 结果可通过验证', async () => {
|
|
107
102
|
const result = await validate({
|
|
108
103
|
timestamp: '2026-07-09 08:00:00',
|
|
109
104
|
level: 'info',
|
|
110
105
|
service: 'backend',
|
|
111
106
|
instanceId: 'blue',
|
|
112
107
|
env: 'production',
|
|
113
|
-
message: 'request
|
|
114
|
-
body: {
|
|
108
|
+
message: 'request failed',
|
|
109
|
+
body: { data: 'original-request' },
|
|
110
|
+
response: { code: 'INVALID_DATA', message: '参数不合法' },
|
|
111
|
+
statusCode: 422,
|
|
112
|
+
result: 'failure',
|
|
115
113
|
});
|
|
116
114
|
|
|
117
|
-
assert.
|
|
118
|
-
assert.match(result.stderr, /truncation/);
|
|
115
|
+
assert.equal(result.status, 0, result.stderr);
|
|
119
116
|
});
|
|
120
117
|
|
|
121
118
|
test('UTC ISO timestamp 时明确失败', async () => {
|
package/.well-known/skills/wdyy-logging-standard/templates/frontend-error-report.template.ts
CHANGED
|
@@ -7,16 +7,11 @@ type FrontendErrorReport = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export async function reportFrontendError(report: FrontendErrorReport): Promise<void> {
|
|
10
|
-
const payload = {
|
|
11
|
-
...report,
|
|
12
|
-
message: report.message.slice(0, 500),
|
|
13
|
-
stack: report.stack?.slice(0, 2000),
|
|
14
|
-
};
|
|
15
10
|
const response = await fetch('/api/client-errors', {
|
|
16
11
|
method: 'POST',
|
|
17
12
|
headers: { 'Content-Type': 'application/json' },
|
|
18
13
|
credentials: 'same-origin',
|
|
19
|
-
body: JSON.stringify(
|
|
14
|
+
body: JSON.stringify(report),
|
|
20
15
|
});
|
|
21
16
|
if (!response.ok) throw new Error(`Frontend error report failed: ${response.status}`);
|
|
22
17
|
}
|
|
@@ -6,6 +6,7 @@ export type LogContext = {
|
|
|
6
6
|
statusCode?: number;
|
|
7
7
|
query?: Record<string, unknown>;
|
|
8
8
|
body?: unknown;
|
|
9
|
+
response?: unknown;
|
|
9
10
|
durationMs?: number;
|
|
10
11
|
errorCode?: string;
|
|
11
12
|
params?: Record<string, unknown>;
|
|
@@ -21,24 +22,16 @@ export type LogEntry = LogContext & {
|
|
|
21
22
|
result?: 'success' | 'failure';
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
'password', 'token', 'secret', 'authorization', 'databaseurl', 'idcard', 'bankcard',
|
|
26
|
-
]);
|
|
25
|
+
const pad = (value: number) => String(value).padStart(2, '0');
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
if (Array.isArray(value)) {
|
|
30
|
-
const items = value.slice(0, 500).map(redact);
|
|
31
|
-
return value.length > 500
|
|
32
|
-
? [...items, '[TRUNCATED ' + (value.length - 500) + ' ITEMS]']
|
|
33
|
-
: items;
|
|
34
|
-
}
|
|
35
|
-
if (!value || typeof value !== 'object') return value;
|
|
36
|
-
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => (
|
|
37
|
-
sensitiveKeys.has(key.toLowerCase()) ? [] : [[key, redact(item)]]
|
|
38
|
-
)));
|
|
39
|
-
};
|
|
27
|
+
type RuntimeEnvironment = Record<string, string | undefined>;
|
|
40
28
|
|
|
41
|
-
const
|
|
29
|
+
const getRuntimeEnvironment = (): RuntimeEnvironment => {
|
|
30
|
+
const runtime = globalThis as typeof globalThis & {
|
|
31
|
+
process?: { env?: RuntimeEnvironment };
|
|
32
|
+
};
|
|
33
|
+
return runtime.process?.env ?? {};
|
|
34
|
+
};
|
|
42
35
|
|
|
43
36
|
export const formatLocalTimestamp = (date = new Date()): string => (
|
|
44
37
|
[date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join('-')
|
|
@@ -46,9 +39,10 @@ export const formatLocalTimestamp = (date = new Date()): string => (
|
|
|
46
39
|
+ [pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds())].join(':')
|
|
47
40
|
);
|
|
48
41
|
|
|
49
|
-
export const createLogFileName = (date = new Date()): string =>
|
|
50
|
-
formatLocalTimestamp(date).replace(' ', '_').replaceAll(':', '-')
|
|
51
|
-
|
|
42
|
+
export const createLogFileName = (date = new Date(), collisionIndex = 0): string => {
|
|
43
|
+
const baseName = formatLocalTimestamp(date).replace(' ', '_').replaceAll(':', '-');
|
|
44
|
+
return `${baseName}${collisionIndex > 0 ? `_${collisionIndex}` : ''}.log`;
|
|
45
|
+
};
|
|
52
46
|
|
|
53
47
|
const resultForStatusCode = (statusCode: number): LogEntry['result'] => {
|
|
54
48
|
if (statusCode >= 100 && statusCode <= 399) return 'success';
|
|
@@ -57,18 +51,18 @@ const resultForStatusCode = (statusCode: number): LogEntry['result'] => {
|
|
|
57
51
|
};
|
|
58
52
|
|
|
59
53
|
export const toLogEntry = (level: LogEntry['level'], message: string, context: LogContext): LogEntry => {
|
|
60
|
-
const
|
|
61
|
-
const result =
|
|
54
|
+
const environment = getRuntimeEnvironment();
|
|
55
|
+
const result = context.statusCode === undefined
|
|
62
56
|
? undefined
|
|
63
|
-
: resultForStatusCode(
|
|
57
|
+
: resultForStatusCode(context.statusCode);
|
|
64
58
|
return {
|
|
65
59
|
timestamp: formatLocalTimestamp(),
|
|
66
60
|
level,
|
|
67
|
-
service:
|
|
68
|
-
instanceId:
|
|
69
|
-
env:
|
|
61
|
+
service: environment.SERVICE_NAME ?? 'backend',
|
|
62
|
+
instanceId: environment.INSTANCE_ID ?? 'local',
|
|
63
|
+
env: environment.NODE_ENV ?? 'development',
|
|
70
64
|
message,
|
|
71
|
-
...
|
|
65
|
+
...context,
|
|
72
66
|
...(result ? { result } : {}),
|
|
73
67
|
};
|
|
74
68
|
};
|