-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevlog.ts
More file actions
140 lines (131 loc) · 6.4 KB
/
Copy pathdevlog.ts
File metadata and controls
140 lines (131 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* 지금 어디에 붙어 있는지, 방금 무엇이 오갔는지 눈으로 보게 한다.
*
* 화면만 봐서는 목인지 실서버인지 구분할 방법이 없다. 둘 다 그럴듯한 답을
* 돌려주기 때문이다. 그래서 실제로 나간 요청을 여기 모아 두고,
*
* 화면 오른쪽 아래 패널 지금 붙어 있는지, 방금 무엇이 오갔는지 (한 줄씩)
* 주소 뒤 ?log=side 앱 옆에 나란히 — 누를 때마다 쌓이는 게 보인다
* 주소 뒤 ?log=1 오간 것 전체를 본문까지 펼쳐 보는 화면
*
* 셋으로 보여 준다. 뒤의 둘이 "이 문장이 서버에서 온 것이다" 를 확인하는 자리다.
*
* 팀 백엔드 모드(npm run dev:team · build:team)에서만 기록한다.
* 기본 빌드에서는 __TEAM_BACKEND__ 가 거짓이라 이 모듈을 부르는 쪽이 통째로 사라진다.
*/
export interface 오간것 {
방법: string;
경로: string;
상태: number | "실패";
걸린시간: number;
시각: number;
/** 보낸 본문. 계정 경로는 남기지 않는다 — 아래 `본문을남길까` 참고. */
요청?: string;
/** 받은 본문. 같은 이유로 계정 경로는 남기지 않는다. */
응답?: string;
/** 본문을 일부러 안 남긴 경우. 화면이 "안 남김" 과 "비어 있음" 을 구분해서 말하게 한다. */
가림?: true;
}
/**
* 이 경로의 본문을 남겨도 되는가.
*
* 계정 경로에는 비밀번호가 실린다. 이 기록은 화면 구석 패널과 ?log=1 화면에
* 그대로 뜨므로, 남기는 순간 어깨 너머로 보인다. 남기지 않는다.
*
* 화이트리스트가 아니라 블랙리스트인 이유 — 새 경로가 생겼을 때 기록이 빠지는
* 것보다 기록이 남는 쪽이 낫다. 비밀이 실릴 경로는 계정 계열뿐이고, 그건
* 여기 적혀 있다. 새로 생기면 여기에 더한다.
*/
export const 본문을남길까 = (경로: string): boolean => !/\/api\/v1\/auth\//.test(경로);
/**
* 본문에서 자유 입력 값을 가린다.
*
* 이 앱에서 사용자가 아무거나 적을 수 있는 칸은 `memo` 하나다("얼음 적게").
* 저장 전에 전화번호.주민등록번호.주소 모양은 걸러 막지만, **이름은 모양이
* 없어서 못 거른다.** 그런데 이 기록은 요청 본문을 통째로 들고 있고 화면과
* 시연 녹화에 그대로 뜬다 — 못 거른 것이 그대로 보이고 최대 60건 남는다.
*
* 무엇을 보냈는지는 알아야 하니 칸 자체는 남기고 값만 가린다. 로그의 쓸모는
* "서버가 준 값이 화면의 이 문장이 됐다" 를 보이는 것이라, memo 값이 보일
* 이유가 없다 — 그건 사용자가 적은 것이지 서버가 준 것이 아니다.
*
* 어디에 있든 가린다. profile-normalizations 의 profileInput, approve 의
* profile, users/{id}/profiles 의 본문에 각각 다른 깊이로 들어 있다.
*/
/*
* label 도 함께 가린다.
*
* 승인 응답의 runSteps[].label 은 검증되지 않은 서버 값이다. 화면에 올리는
* 쪽은 아는 값만 통과시키도록 막았는데, **이 기록은 응답 본문을 통째로**
* 담는다. 서버가 상품 ID 나 화면 좌표를 보내면 화면에는 안 뜨지만 여기
* 최대 60건이 남고, ?log 화면과 시연 녹화에 그대로 보인다.
*
* "앱이 상품 ID 를 들고 있으면 안 된다" 는 화면만이 아니라 메모리에 대한
* 조건이라, 가리는 쪽이 맞다.
*
* 무엇을 했는지는 action 과 success 로 남으므로 흐름을 보는 데는 지장이 없다.
* 잃는 것은 '어떤 값을 골랐는지' 한 조각인데, 그건 요청 본문 쪽에 이미 있다.
*/
const 가릴칸 = new Set(["memo", "label"]);
const 걸어가며가리기 = (v: unknown): unknown => {
if (Array.isArray(v)) return v.map(걸어가며가리기);
if (v && typeof v === "object") {
return Object.fromEntries(
Object.entries(v as Record<string, unknown>).map(([k, x]) =>
가릴칸.has(k) ? [k, "***"] : [k, 걸어가며가리기(x)]),
);
}
return v;
};
const 가린본문 = (본문: string | undefined): string | undefined => {
if (!본문) return 본문;
try {
return JSON.stringify(걸어가며가리기(JSON.parse(본문)));
} catch {
/*
* JSON 이 아니면 통째로 버린다. 정규식으로 긁어내는 방법도 있지만,
* 모양을 모르는 본문에서 무엇이 남는지 장담할 수 없다. 가린다고 해 놓고
* 일부가 새는 것보다, 못 가리는 본문은 아예 안 보여 주는 편이 낫다.
*/
return "(JSON 이 아니라 가리지 못했습니다 — 남기지 않습니다)";
}
};
/**
* 경로에 실린 userId 를 가린다.
*
* 이 기록은 구석 패널과 ?log 화면에 그대로 뜬다. 시연 화면이나 녹화에도 같이 찍힌다.
* 그런데 백엔드가 토큰을 발급하지 않아서 **userId 자체가 열쇠다** — 숫자만 알면
* 그 사람의 주문표를 읽고 쓸 수 있다(docs/BACKEND_INTEGRATION.md 의 ②).
* 화면에 띄우는 것이 곧 그 열쇠를 보여 주는 일이 된다.
*
* 무엇을 불렀는지는 그대로 알 수 있게 두고, 누구인지만 가린다.
* /api/v1/users/12345/profiles -> /api/v1/users/***\/profiles
*
* 남기는 자리 한 곳에서 가린다. 부르는 쪽마다 하면 한 곳을 빠뜨리는 날이 온다.
*/
const 가린경로 = (경로: string): string => 경로.replace(/\/users\/[^/]+\//, "/users/***/");
/** ?log=1 화면이 흐름을 보여 줄 수 있을 만큼. 개발용이라 이 정도면 넉넉하다. */
const 최대 = 60;
let 기록: 오간것[] = [];
const 듣는이 = new Set<() => void>();
export const 연동기록 = {
남기기(x: 오간것): void {
// 오래된 것부터 버린다. 개발용이라 다 쌓아 둘 이유가 없다.
기록 = [
{ ...x, 경로: 가린경로(x.경로), 요청: 가린본문(x.요청), 응답: 가린본문(x.응답) },
...기록,
].slice(0, 최대);
for (const f of 듣는이) f();
},
읽기: (): 오간것[] => 기록,
비우기(): void {
기록 = [];
for (const f of 듣는이) f();
},
구독(f: () => void): () => void {
듣는이.add(f);
return () => { 듣는이.delete(f); };
},
};
/** 팀 백엔드 모드인가. 빌드할 때 정해지므로 아닌 쪽 코드는 번들에서 빠진다. */
export const 팀백엔드모드 = __TEAM_BACKEND__;