jskelet 0.4.6 → 0.4.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/CHANGELOG.md +10 -0
- package/docs/06-cache.md +64 -10
- package/docs/07-yapilandirma.md +24 -1
- package/docs/09-dev-araclari.md +11 -5
- package/docs/en/06-caching.md +65 -13
- package/docs/en/07-configuration.md +24 -1
- package/docs/en/09-dev-tools.md +11 -6
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +194 -5
- package/src/client/devtools/report.js +20 -0
- package/src/client/registry.js +8 -0
- package/src/config/defaults.js +40 -1
- package/src/config/index.js +106 -1
- package/src/http/request-context.js +4 -1
- package/src/index.js +1 -1
- package/src/server/dev/devtools.js +118 -5
- package/src/server/dev/report.js +131 -15
- package/src/server/html-cache.js +14 -0
- package/src/server/prewarm.js +268 -19
- package/src/server/render.js +22 -3
|
@@ -13,6 +13,7 @@ import { createHash } from "node:crypto";
|
|
|
13
13
|
import express from "express";
|
|
14
14
|
import * as log from "../../log.mjs";
|
|
15
15
|
import { FRAMEWORK_ROOT, getConfig } from "../../config/index.js";
|
|
16
|
+
import { getRequestContext } from "../../http/request-context.js";
|
|
16
17
|
import { prewarm, prewarmProgress } from "../prewarm.js";
|
|
17
18
|
import { clearHtmlCache } from "../html-cache.js";
|
|
18
19
|
import {
|
|
@@ -55,7 +56,21 @@ const STATE_FILE = path.join(
|
|
|
55
56
|
/** @type {{ id: number, method: string, url: string, status: number, ms: number, cache: string | null, at: number }[]} */
|
|
56
57
|
let requests = [];
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* id: number,
|
|
62
|
+
* level: string,
|
|
63
|
+
* message: string,
|
|
64
|
+
* stack: string | null,
|
|
65
|
+
* url: string | null,
|
|
66
|
+
* page: string | null,
|
|
67
|
+
* island: string | null,
|
|
68
|
+
* details: unknown,
|
|
69
|
+
* at: number,
|
|
70
|
+
* }} ServerError
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/** @type {ServerError[]} */
|
|
59
74
|
let errors = [];
|
|
60
75
|
|
|
61
76
|
let nextId = 1;
|
|
@@ -103,7 +118,13 @@ function trim(list) {
|
|
|
103
118
|
/**
|
|
104
119
|
* @param {string} level
|
|
105
120
|
* @param {string} message
|
|
106
|
-
* @param {{
|
|
121
|
+
* @param {{
|
|
122
|
+
* stack?: string | null,
|
|
123
|
+
* url?: string | null,
|
|
124
|
+
* page?: string | null,
|
|
125
|
+
* island?: string | null,
|
|
126
|
+
* details?: unknown,
|
|
127
|
+
* }} [extra]
|
|
107
128
|
*/
|
|
108
129
|
export function recordServerError(level, message, extra = {}) {
|
|
109
130
|
errors.push({
|
|
@@ -112,6 +133,9 @@ export function recordServerError(level, message, extra = {}) {
|
|
|
112
133
|
message,
|
|
113
134
|
stack: extra.stack ?? null,
|
|
114
135
|
url: extra.url ?? null,
|
|
136
|
+
page: extra.page ?? null,
|
|
137
|
+
island: extra.island ?? null,
|
|
138
|
+
details: extra.details ?? null,
|
|
115
139
|
at: Date.now(),
|
|
116
140
|
});
|
|
117
141
|
trim(errors);
|
|
@@ -121,21 +145,49 @@ export function recordServerError(level, message, extra = {}) {
|
|
|
121
145
|
|
|
122
146
|
/**
|
|
123
147
|
* `console.error` / `console.warn` çıktısını da overlay'e taşır: sunucudaki
|
|
124
|
-
* uyarılar terminalde kaybolmasın.
|
|
148
|
+
* uyarılar terminalde kaybolmasın. Render bağlamındaysa sayfa yolu da yazılır.
|
|
125
149
|
*/
|
|
126
150
|
function patchConsole() {
|
|
127
151
|
for (const level of /** @type {const} */ (["error", "warn"])) {
|
|
128
152
|
const original = console[level].bind(console);
|
|
129
153
|
console[level] = (...args) => {
|
|
130
154
|
const error = args.find((arg) => arg instanceof Error);
|
|
155
|
+
const page = currentPage();
|
|
131
156
|
recordServerError(level, args.map(format).join(" "), {
|
|
132
157
|
stack: error?.stack ?? null,
|
|
158
|
+
page,
|
|
159
|
+
url: page,
|
|
160
|
+
details: extractDetails(args),
|
|
133
161
|
});
|
|
134
162
|
original(...args);
|
|
135
163
|
};
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
|
|
167
|
+
/** @returns {string | null} */
|
|
168
|
+
function currentPage() {
|
|
169
|
+
return getRequestContext()?.pathname ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* console argümanlarından yapılandırılmış bir `details` alanı ayıklar.
|
|
174
|
+
* Uygulama `console.error("msg", { details: {...} })` yazdığında overlay
|
|
175
|
+
* nesneyi `[object Object]` yerine açılabilir JSON olarak görsün.
|
|
176
|
+
*
|
|
177
|
+
* @param {unknown[]} args
|
|
178
|
+
* @returns {unknown}
|
|
179
|
+
*/
|
|
180
|
+
function extractDetails(args) {
|
|
181
|
+
for (const arg of args) {
|
|
182
|
+
if (!arg || typeof arg !== "object" || arg instanceof Error) continue;
|
|
183
|
+
const record = /** @type {Record<string, unknown>} */ (arg);
|
|
184
|
+
if ("details" in record) return record.details;
|
|
185
|
+
if ("detail" in record) return record.detail;
|
|
186
|
+
return record;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
139
191
|
/**
|
|
140
192
|
* @param {unknown} value
|
|
141
193
|
* @returns {string}
|
|
@@ -144,12 +196,73 @@ function format(value) {
|
|
|
144
196
|
if (typeof value === "string") return value;
|
|
145
197
|
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
146
198
|
try {
|
|
147
|
-
return JSON.stringify(value)
|
|
199
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
200
|
+
// Döngüsel referanslarda stringify zaten fırlar; burada yalnızca
|
|
201
|
+
// Error örneklerini okunabilir kılmak yeterli.
|
|
202
|
+
if (nested instanceof Error) {
|
|
203
|
+
return { name: nested.name, message: nested.message };
|
|
204
|
+
}
|
|
205
|
+
return nested;
|
|
206
|
+
});
|
|
148
207
|
} catch {
|
|
149
208
|
return String(value);
|
|
150
209
|
}
|
|
151
210
|
}
|
|
152
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Upstream `fetch` başarısızlığını overlay hata listesine yazar.
|
|
214
|
+
*
|
|
215
|
+
* @param {{
|
|
216
|
+
* url: string,
|
|
217
|
+
* method: string,
|
|
218
|
+
* status: number,
|
|
219
|
+
* ms: number,
|
|
220
|
+
* bytes: number,
|
|
221
|
+
* error: string | null,
|
|
222
|
+
* page: string | null,
|
|
223
|
+
* details: unknown,
|
|
224
|
+
* }} call
|
|
225
|
+
*/
|
|
226
|
+
function recordApiFailure(call) {
|
|
227
|
+
// Aynı SSR turunda hem fetch sarmalayıcısı hem uygulama logger'ı aynı
|
|
228
|
+
// hatayı basabiliyor; kısa pencerede tekilleştir.
|
|
229
|
+
const last = errors.at(-1);
|
|
230
|
+
if (
|
|
231
|
+
last &&
|
|
232
|
+
last.url === call.url &&
|
|
233
|
+
last.page === call.page &&
|
|
234
|
+
Date.now() - last.at < 2000
|
|
235
|
+
) {
|
|
236
|
+
// İlk kayıtta details yoksa sonrakinin gövdesini birleştir.
|
|
237
|
+
if (last.details == null && call.details != null) {
|
|
238
|
+
last.details = call.details;
|
|
239
|
+
if (call.error && !last.message.includes(call.error)) {
|
|
240
|
+
last.message = `${call.method} ${shortApiPath(call.url)} → ${call.error}`;
|
|
241
|
+
}
|
|
242
|
+
persist();
|
|
243
|
+
pushStats();
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
recordServerError("error", `${call.method} ${shortApiPath(call.url)} → ${call.error ?? call.status}`, {
|
|
249
|
+
url: call.url,
|
|
250
|
+
page: call.page,
|
|
251
|
+
details: call.details,
|
|
252
|
+
stack: null,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @param {string} url */
|
|
257
|
+
function shortApiPath(url) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = new URL(url);
|
|
260
|
+
return parsed.pathname + parsed.search;
|
|
261
|
+
} catch {
|
|
262
|
+
return url;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
153
266
|
/** Her HTML isteğinin süresini ve cache durumunu kaydeder. */
|
|
154
267
|
function timing() {
|
|
155
268
|
/** @type {import('express').RequestHandler} */
|
|
@@ -448,7 +561,7 @@ export function mountDevtools(app) {
|
|
|
448
561
|
|
|
449
562
|
restore();
|
|
450
563
|
patchConsole();
|
|
451
|
-
trackServerFetch();
|
|
564
|
+
trackServerFetch({ onFailure: recordApiFailure });
|
|
452
565
|
watchManifest();
|
|
453
566
|
startVersionCheck();
|
|
454
567
|
startHeartbeat();
|
package/src/server/dev/report.js
CHANGED
|
@@ -18,6 +18,7 @@ import { getRedisStatus } from "../redis.js";
|
|
|
18
18
|
import { getUpstreamLimiterStatus } from "../upstream-limiter.js";
|
|
19
19
|
import { prewarmProgress } from "../prewarm.js";
|
|
20
20
|
import { getConfig } from "../../config/index.js";
|
|
21
|
+
import { getRequestContext } from "../../http/request-context.js";
|
|
21
22
|
|
|
22
23
|
/** Yollar config'ten: framework paket içine taşındığında `../..` sayan her hesap bozulur. */
|
|
23
24
|
const ROOT = getConfig().root;
|
|
@@ -78,61 +79,176 @@ export function clearPageReports() {
|
|
|
78
79
|
/* --------------------------------------------------- sunucu tarafı API çağrıları */
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
* @
|
|
82
|
-
*
|
|
82
|
+
* @typedef {{
|
|
83
|
+
* url: string,
|
|
84
|
+
* host: string,
|
|
85
|
+
* method: string,
|
|
86
|
+
* status: number,
|
|
87
|
+
* ms: number,
|
|
88
|
+
* bytes: number,
|
|
89
|
+
* at: number,
|
|
90
|
+
* error: string | null,
|
|
91
|
+
* page: string | null,
|
|
92
|
+
* details: unknown,
|
|
93
|
+
* }} ServerApiCall
|
|
83
94
|
*/
|
|
95
|
+
|
|
96
|
+
/** @type {ServerApiCall[]} */
|
|
84
97
|
const serverApiCalls = [];
|
|
85
98
|
const MAX_API_CALLS = 300;
|
|
86
99
|
|
|
100
|
+
/** Başarısız çağrı gövdesinin overlay'e taşınacak üst sınırı. */
|
|
101
|
+
const DETAILS_MAX = 4_000;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @typedef {{
|
|
105
|
+
* url: string,
|
|
106
|
+
* method: string,
|
|
107
|
+
* status: number,
|
|
108
|
+
* ms: number,
|
|
109
|
+
* bytes: number,
|
|
110
|
+
* error: string | null,
|
|
111
|
+
* page: string | null,
|
|
112
|
+
* details: unknown,
|
|
113
|
+
* }} ApiFailure
|
|
114
|
+
*/
|
|
115
|
+
|
|
87
116
|
/**
|
|
88
117
|
* SSR sırasında yapılan dış çağrıları ölçer. `globalThis.fetch` sarılır;
|
|
89
118
|
* yalnızca dev'de çağrıldığı için üretim yolu dokunulmaz kalır.
|
|
119
|
+
*
|
|
120
|
+
* Başarısız cevaplar (4xx/5xx ya da ağ) isteğe bağlı `onFailure` ile
|
|
121
|
+
* overlay hata günlüğüne de düşer — uygulama kendi logger'ıyla stderr'e
|
|
122
|
+
* yazsa bile panel "hangi sayfa hangi API" bilgisini görsün.
|
|
123
|
+
*
|
|
124
|
+
* @param {{ onFailure?: (call: ApiFailure) => void }} [options]
|
|
90
125
|
*/
|
|
91
|
-
export function trackServerFetch() {
|
|
126
|
+
export function trackServerFetch(options = {}) {
|
|
92
127
|
const original = globalThis.fetch;
|
|
93
|
-
if (original.__jskeletWrapped) return;
|
|
128
|
+
if (/** @type {any} */ (original).__jskeletWrapped) return;
|
|
129
|
+
|
|
130
|
+
const { onFailure } = options;
|
|
94
131
|
|
|
95
132
|
/** @type {typeof fetch} */
|
|
96
133
|
const wrapped = async (input, init) => {
|
|
97
134
|
const url = typeof input === "string" ? input : (input?.url ?? String(input));
|
|
135
|
+
const method = String(init?.method ?? "GET").toUpperCase();
|
|
98
136
|
const started = Date.now();
|
|
137
|
+
const page = currentPage();
|
|
99
138
|
|
|
100
139
|
// Kendi sunucumuza yapılan istekler (ısıtma, sağlık kontrolü) API sayılmaz.
|
|
101
|
-
const isSelf = /^https?:\/\/(127\.0\.0\.1|localhost)/i.test(url);
|
|
140
|
+
const isSelf = /^https?:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:|\/|$)/i.test(url);
|
|
102
141
|
|
|
103
142
|
try {
|
|
104
143
|
const response = await original(input, init);
|
|
105
144
|
if (!isSelf) {
|
|
106
|
-
|
|
145
|
+
const details = response.ok
|
|
146
|
+
? null
|
|
147
|
+
: await readFailureDetails(response);
|
|
148
|
+
const error = response.ok ? null : summarizeFailure(response.status, details);
|
|
149
|
+
const call = {
|
|
107
150
|
url,
|
|
108
|
-
method
|
|
151
|
+
method,
|
|
109
152
|
status: response.status,
|
|
110
153
|
ms: Date.now() - started,
|
|
111
154
|
bytes: Number(response.headers.get("content-length") ?? 0),
|
|
112
|
-
error
|
|
113
|
-
|
|
155
|
+
error,
|
|
156
|
+
page,
|
|
157
|
+
details,
|
|
158
|
+
};
|
|
159
|
+
push(call);
|
|
160
|
+
if (error) onFailure?.(call);
|
|
114
161
|
}
|
|
115
162
|
return response;
|
|
116
163
|
} catch (error) {
|
|
117
164
|
if (!isSelf) {
|
|
118
|
-
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
const call = {
|
|
119
167
|
url,
|
|
120
|
-
method
|
|
168
|
+
method,
|
|
121
169
|
status: 0,
|
|
122
170
|
ms: Date.now() - started,
|
|
123
171
|
bytes: 0,
|
|
124
|
-
error:
|
|
125
|
-
|
|
172
|
+
error: message,
|
|
173
|
+
page,
|
|
174
|
+
details: null,
|
|
175
|
+
};
|
|
176
|
+
push(call);
|
|
177
|
+
onFailure?.(call);
|
|
126
178
|
}
|
|
127
179
|
throw error;
|
|
128
180
|
}
|
|
129
181
|
};
|
|
130
182
|
|
|
131
|
-
wrapped.__jskeletWrapped = true;
|
|
183
|
+
/** @type {any} */ (wrapped).__jskeletWrapped = true;
|
|
132
184
|
globalThis.fetch = wrapped;
|
|
133
185
|
}
|
|
134
186
|
|
|
135
|
-
/** @
|
|
187
|
+
/** @returns {string | null} */
|
|
188
|
+
function currentPage() {
|
|
189
|
+
return getRequestContext()?.pathname ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {Response} response
|
|
194
|
+
* @returns {Promise<unknown>}
|
|
195
|
+
*/
|
|
196
|
+
async function readFailureDetails(response) {
|
|
197
|
+
try {
|
|
198
|
+
const text = (await response.clone().text()).slice(0, DETAILS_MAX);
|
|
199
|
+
if (!text) return null;
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(text);
|
|
202
|
+
} catch {
|
|
203
|
+
return text;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Overlay başlığı: durum + varsa API'nin kendi doğrulama mesajı.
|
|
212
|
+
*
|
|
213
|
+
* @param {number} status
|
|
214
|
+
* @param {unknown} details
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
function summarizeFailure(status, details) {
|
|
218
|
+
const label = `HTTP ${status}`;
|
|
219
|
+
const detail = failureMessage(details);
|
|
220
|
+
return detail ? `${label}: ${detail}` : label;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Upstream gövdesinden kısa, okunabilir bir cümle çıkarır. `[object Object]`
|
|
225
|
+
* basmamak için nesneleri bilinçli dolaşır.
|
|
226
|
+
*
|
|
227
|
+
* @param {unknown} details
|
|
228
|
+
* @returns {string | null}
|
|
229
|
+
*/
|
|
230
|
+
function failureMessage(details) {
|
|
231
|
+
if (details == null) return null;
|
|
232
|
+
if (typeof details === "string") return details.slice(0, 280);
|
|
233
|
+
if (typeof details !== "object") return String(details);
|
|
234
|
+
|
|
235
|
+
const record = /** @type {Record<string, unknown>} */ (details);
|
|
236
|
+
for (const key of ["details", "detail", "message", "error", "title"]) {
|
|
237
|
+
const value = record[key];
|
|
238
|
+
if (typeof value === "string" && value.trim()) return value.slice(0, 280);
|
|
239
|
+
if (value && typeof value === "object") {
|
|
240
|
+
const nested = failureMessage(value);
|
|
241
|
+
if (nested) return nested;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return JSON.stringify(details).slice(0, 280);
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @param {Omit<ServerApiCall, "host" | "at">} call */
|
|
136
252
|
function push(call) {
|
|
137
253
|
let host = "—";
|
|
138
254
|
try {
|
package/src/server/html-cache.js
CHANGED
|
@@ -815,3 +815,17 @@ export function getHtmlCacheEntries() {
|
|
|
815
815
|
deps: entry.deps.size,
|
|
816
816
|
}));
|
|
817
817
|
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Yol (query'siz) için taze bir HTML girdisi var mı? Ziyaret ısıtması yalnızca
|
|
821
|
+
* soğuk / bayat hedefleri kuyruğa alır; HIT'leri yeniden çekmez.
|
|
822
|
+
*
|
|
823
|
+
* @param {string} pathname
|
|
824
|
+
* @returns {boolean}
|
|
825
|
+
*/
|
|
826
|
+
export function isHtmlCacheFresh(pathname) {
|
|
827
|
+
if (typeof pathname !== "string" || !pathname.startsWith("/")) return false;
|
|
828
|
+
const entry = store.get(pathname);
|
|
829
|
+
if (!entry) return false;
|
|
830
|
+
return Date.now() < entry.expiresAt;
|
|
831
|
+
}
|