silgi 0.1.0-beta.2 → 0.1.0-beta.4
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/core/handler.mjs +23 -2
- package/dist/integrations/better-auth/index.d.mts +61 -0
- package/dist/integrations/better-auth/index.mjs +332 -0
- package/dist/integrations/drizzle/index.d.mts +27 -0
- package/dist/integrations/drizzle/index.mjs +286 -0
- package/dist/plugins/analytics.d.mts +22 -1
- package/dist/plugins/analytics.mjs +20 -6
- package/lib/dashboard/index.html +3 -3
- package/package.json +11 -7
- package/dist/adapters/elysia.d.mts +0 -17
- package/dist/adapters/elysia.mjs +0 -76
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
//#region src/integrations/drizzle/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Silgi + Drizzle ORM tracing integration.
|
|
5
|
+
*
|
|
6
|
+
* Wraps Drizzle session methods to intercept every query and record
|
|
7
|
+
* spans in silgi analytics. Uses AsyncLocalStorage to bridge request
|
|
8
|
+
* context to the DB layer.
|
|
9
|
+
*
|
|
10
|
+
* Patching priority (inspired by @kubiks/otel-drizzle):
|
|
11
|
+
* 1. `db.session.prepareQuery` — wrap returned prepared.execute (main ORM path)
|
|
12
|
+
* 2. `db.session.query` — for direct string queries
|
|
13
|
+
* 3. `db.session.transaction` — re-instrument tx session per transaction call
|
|
14
|
+
* 4. `db.$client.query` or `db.$client.execute` — fallback to raw driver
|
|
15
|
+
* 5. `db._.session.execute` — deep internal fallback
|
|
16
|
+
*
|
|
17
|
+
* Instance patching, NOT prototype patching. Idempotent via flag.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { instrumentDrizzle, withSilgiCtx } from 'silgi/drizzle'
|
|
22
|
+
*
|
|
23
|
+
* const db = instrumentDrizzle(drizzle(url, { schema }), {
|
|
24
|
+
* dbName: 'ecommerce',
|
|
25
|
+
* peerName: 'db.example.com',
|
|
26
|
+
* peerPort: 5432,
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* const listUsers = s.$resolve(async ({ ctx }) => {
|
|
30
|
+
* return withSilgiCtx(ctx, () => db.select().from(users))
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
const ctxStorage = new AsyncLocalStorage();
|
|
35
|
+
const INSTRUMENTED = "__silgiDrizzleInstrumented";
|
|
36
|
+
const DEFAULT_DB_SYSTEM = "postgresql";
|
|
37
|
+
const DEFAULT_MAX_QUERY_LENGTH = 1e3;
|
|
38
|
+
/**
|
|
39
|
+
* Instrument a Drizzle db instance to record query spans in silgi analytics.
|
|
40
|
+
* Returns the same db instance (mutated). Safe to call multiple times.
|
|
41
|
+
*/
|
|
42
|
+
function instrumentDrizzle(db, config) {
|
|
43
|
+
if (!db || db[INSTRUMENTED]) return db;
|
|
44
|
+
const cfg = resolveConfig(config);
|
|
45
|
+
let instrumented = false;
|
|
46
|
+
const session = db.session ?? db._?.session;
|
|
47
|
+
if (session) instrumented = patchSession(session, cfg, false);
|
|
48
|
+
if (!instrumented && db.$client) instrumented = patchRawClient(db.$client, cfg);
|
|
49
|
+
if (!instrumented && db._?.session && typeof db._.session.execute === "function") instrumented = patchSessionExecute(db._.session, cfg);
|
|
50
|
+
if (!instrumented) {
|
|
51
|
+
console.warn("[silgi/drizzle] Could not find any patchable method — skipping instrumentation");
|
|
52
|
+
return db;
|
|
53
|
+
}
|
|
54
|
+
db[INSTRUMENTED] = true;
|
|
55
|
+
return db;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a function with silgi context available to instrumented Drizzle instances.
|
|
59
|
+
* All Drizzle queries inside `fn` will be recorded as trace spans.
|
|
60
|
+
*/
|
|
61
|
+
function withSilgiCtx(ctx, fn) {
|
|
62
|
+
return ctxStorage.run(ctx, fn);
|
|
63
|
+
}
|
|
64
|
+
function resolveConfig(config) {
|
|
65
|
+
return {
|
|
66
|
+
dbSystem: config?.dbSystem ?? DEFAULT_DB_SYSTEM,
|
|
67
|
+
dbName: config?.dbName,
|
|
68
|
+
captureQueryText: config?.captureQueryText !== false,
|
|
69
|
+
maxQueryTextLength: config?.maxQueryTextLength ?? DEFAULT_MAX_QUERY_LENGTH,
|
|
70
|
+
peerName: config?.peerName,
|
|
71
|
+
peerPort: config?.peerPort
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Patch session.prepareQuery, session.query, and session.transaction.
|
|
76
|
+
* Returns true if any method was patched.
|
|
77
|
+
*/
|
|
78
|
+
function patchSession(session, cfg, isTx) {
|
|
79
|
+
const flagSuffix = isTx ? "_tx" : "";
|
|
80
|
+
if (session[INSTRUMENTED + flagSuffix]) return true;
|
|
81
|
+
let patched = false;
|
|
82
|
+
if (typeof session.prepareQuery === "function") {
|
|
83
|
+
const originalPrepareQuery = session.prepareQuery.bind(session);
|
|
84
|
+
session.prepareQuery = function patchedPrepareQuery(...args) {
|
|
85
|
+
const prepared = originalPrepareQuery.apply(this, args);
|
|
86
|
+
if (!prepared || typeof prepared.execute !== "function") return prepared;
|
|
87
|
+
const reqTrace = ctxStorage.getStore()?.__analyticsTrace;
|
|
88
|
+
if (!reqTrace) return prepared;
|
|
89
|
+
const queryText = extractQueryText(args[0]) ?? prepared.rawQueryConfig?.text ?? prepared.queryConfig?.text ?? null;
|
|
90
|
+
const originalExecute = prepared.execute.bind(prepared);
|
|
91
|
+
prepared.execute = function tracedExecute(...execArgs) {
|
|
92
|
+
return traceExecution(reqTrace, cfg, queryText, isTx, originalExecute, this, execArgs);
|
|
93
|
+
};
|
|
94
|
+
return prepared;
|
|
95
|
+
};
|
|
96
|
+
patched = true;
|
|
97
|
+
}
|
|
98
|
+
if (typeof session.query === "function") {
|
|
99
|
+
const originalQuery = session.query.bind(session);
|
|
100
|
+
session.query = function patchedQuery(queryString, params) {
|
|
101
|
+
const reqTrace = ctxStorage.getStore()?.__analyticsTrace;
|
|
102
|
+
if (!reqTrace) return originalQuery.call(this, queryString, params);
|
|
103
|
+
return traceExecution(reqTrace, cfg, queryString ?? null, isTx, originalQuery, this, [queryString, params]);
|
|
104
|
+
};
|
|
105
|
+
patched = true;
|
|
106
|
+
}
|
|
107
|
+
if (!isTx && typeof session.transaction === "function") {
|
|
108
|
+
const originalTransaction = session.transaction.bind(session);
|
|
109
|
+
session.transaction = function patchedTransaction(callback, txConfig) {
|
|
110
|
+
return originalTransaction.call(this, (tx) => {
|
|
111
|
+
const txSession = tx.session ?? tx;
|
|
112
|
+
if (txSession && typeof txSession.prepareQuery === "function") patchSession(txSession, cfg, true);
|
|
113
|
+
return callback(tx);
|
|
114
|
+
}, txConfig);
|
|
115
|
+
};
|
|
116
|
+
patched = true;
|
|
117
|
+
}
|
|
118
|
+
if (patched) session[INSTRUMENTED + flagSuffix] = true;
|
|
119
|
+
return patched;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Patch $client.query or $client.execute as fallback for raw driver access.
|
|
123
|
+
*/
|
|
124
|
+
function patchRawClient(client, cfg) {
|
|
125
|
+
if (!client || client[INSTRUMENTED]) return false;
|
|
126
|
+
const methodName = typeof client.query === "function" ? "query" : typeof client.execute === "function" ? "execute" : null;
|
|
127
|
+
if (!methodName) return false;
|
|
128
|
+
const originalMethod = client[methodName].bind(client);
|
|
129
|
+
client[methodName] = function patchedClientMethod(...args) {
|
|
130
|
+
const reqTrace = ctxStorage.getStore()?.__analyticsTrace;
|
|
131
|
+
if (!reqTrace) return originalMethod.apply(this, args);
|
|
132
|
+
return traceExecution(reqTrace, cfg, extractQueryText(args[0]) ?? null, false, originalMethod, this, args);
|
|
133
|
+
};
|
|
134
|
+
client[INSTRUMENTED] = true;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Patch db._.session.execute as deep internal fallback.
|
|
139
|
+
*/
|
|
140
|
+
function patchSessionExecute(session, cfg) {
|
|
141
|
+
if (session[INSTRUMENTED]) return false;
|
|
142
|
+
const originalExecute = session.execute.bind(session);
|
|
143
|
+
session.execute = function patchedDeepExecute(...args) {
|
|
144
|
+
const reqTrace = ctxStorage.getStore()?.__analyticsTrace;
|
|
145
|
+
if (!reqTrace) return originalExecute.apply(this, args);
|
|
146
|
+
return traceExecution(reqTrace, cfg, extractQueryText(args[0]) ?? null, false, originalExecute, this, args);
|
|
147
|
+
};
|
|
148
|
+
session[INSTRUMENTED] = true;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Execute a function and record a trace span with timing, attributes, and error handling.
|
|
153
|
+
* Handles both sync and async (Promise) return values.
|
|
154
|
+
*/
|
|
155
|
+
function traceExecution(reqTrace, cfg, queryText, isTx, fn, thisArg, args) {
|
|
156
|
+
const spanName = buildSpanName(queryText, isTx);
|
|
157
|
+
const attributes = buildAttributes(cfg, queryText, isTx);
|
|
158
|
+
const start = performance.now();
|
|
159
|
+
try {
|
|
160
|
+
const result = fn.apply(thisArg, args);
|
|
161
|
+
if (result instanceof Promise) return result.then((value) => {
|
|
162
|
+
pushSpan(reqTrace, spanName, start, queryText, cfg, attributes, void 0);
|
|
163
|
+
return value;
|
|
164
|
+
}, (error) => {
|
|
165
|
+
pushSpan(reqTrace, spanName, start, queryText, cfg, attributes, error);
|
|
166
|
+
throw error;
|
|
167
|
+
});
|
|
168
|
+
pushSpan(reqTrace, spanName, start, queryText, cfg, attributes, void 0);
|
|
169
|
+
return result;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
pushSpan(reqTrace, spanName, start, queryText, cfg, attributes, error);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function pushSpan(reqTrace, name, start, queryText, cfg, attributes, error) {
|
|
176
|
+
const detail = cfg.captureQueryText && queryText ? truncateQuery(queryText, cfg.maxQueryTextLength) : void 0;
|
|
177
|
+
reqTrace.spans.push({
|
|
178
|
+
name,
|
|
179
|
+
kind: "db",
|
|
180
|
+
durationMs: round(performance.now() - start),
|
|
181
|
+
startOffsetMs: round(start - reqTrace.t0),
|
|
182
|
+
detail,
|
|
183
|
+
attributes,
|
|
184
|
+
error: error ? formatError(error) : void 0
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function buildAttributes(cfg, queryText, isTx) {
|
|
188
|
+
const attrs = { "db.system": cfg.dbSystem };
|
|
189
|
+
if (cfg.dbName) attrs["db.name"] = cfg.dbName;
|
|
190
|
+
if (cfg.peerName) attrs["net.peer.name"] = cfg.peerName;
|
|
191
|
+
if (cfg.peerPort) attrs["net.peer.port"] = cfg.peerPort;
|
|
192
|
+
if (isTx) attrs["db.transaction"] = true;
|
|
193
|
+
if (queryText) {
|
|
194
|
+
const op = extractOperationName(queryText);
|
|
195
|
+
if (op) attrs["db.operation"] = op;
|
|
196
|
+
if (cfg.captureQueryText) attrs["db.statement"] = truncateQuery(queryText, cfg.maxQueryTextLength);
|
|
197
|
+
}
|
|
198
|
+
return attrs;
|
|
199
|
+
}
|
|
200
|
+
function buildSpanName(queryText, isTx) {
|
|
201
|
+
const prefix = isTx ? "db.tx" : "db";
|
|
202
|
+
if (!queryText) return `${prefix}.query`;
|
|
203
|
+
const opInfo = extractOperationInfo(queryText);
|
|
204
|
+
if (!opInfo) return `${prefix}.query`;
|
|
205
|
+
return opInfo.table ? `${prefix}.${opInfo.op}.${opInfo.table}` : `${prefix}.${opInfo.op}`;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Extract SQL text from various query argument formats:
|
|
209
|
+
* - Plain string
|
|
210
|
+
* - { sql: string }
|
|
211
|
+
* - { text: string }
|
|
212
|
+
* - { queryString: string }
|
|
213
|
+
* - { queryChunks: ..., sql: string }
|
|
214
|
+
*/
|
|
215
|
+
function extractQueryText(queryArg) {
|
|
216
|
+
if (typeof queryArg === "string") return queryArg;
|
|
217
|
+
if (queryArg && typeof queryArg === "object") {
|
|
218
|
+
const q = queryArg;
|
|
219
|
+
if (typeof q.sql === "string") return q.sql;
|
|
220
|
+
if (typeof q.text === "string") return q.text;
|
|
221
|
+
if (typeof q.queryString === "string") return q.queryString;
|
|
222
|
+
if (typeof q.queryChunks === "object" && typeof q.sql === "string") return q.sql;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function truncateQuery(text, maxLength) {
|
|
227
|
+
if (text.length <= maxLength) return text;
|
|
228
|
+
return text.slice(0, maxLength) + "...";
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Parse the SQL operation and target table from query text.
|
|
232
|
+
* Returns lowercase op name and table for span naming.
|
|
233
|
+
*/
|
|
234
|
+
function extractOperationInfo(sql) {
|
|
235
|
+
const upper = sql.trimStart().toUpperCase();
|
|
236
|
+
if (upper.startsWith("SELECT")) return {
|
|
237
|
+
op: "select",
|
|
238
|
+
table: matchTable(sql, /from\s+"?(\w+)"?/i)
|
|
239
|
+
};
|
|
240
|
+
if (upper.startsWith("INSERT")) return {
|
|
241
|
+
op: "insert",
|
|
242
|
+
table: matchTable(sql, /into\s+"?(\w+)"?/i)
|
|
243
|
+
};
|
|
244
|
+
if (upper.startsWith("UPDATE")) return {
|
|
245
|
+
op: "update",
|
|
246
|
+
table: matchTable(sql, /update\s+"?(\w+)"?/i)
|
|
247
|
+
};
|
|
248
|
+
if (upper.startsWith("DELETE")) return {
|
|
249
|
+
op: "delete",
|
|
250
|
+
table: matchTable(sql, /from\s+"?(\w+)"?/i)
|
|
251
|
+
};
|
|
252
|
+
if (upper.startsWith("BEGIN") || upper.startsWith("START TRANSACTION")) return {
|
|
253
|
+
op: "begin",
|
|
254
|
+
table: null
|
|
255
|
+
};
|
|
256
|
+
if (upper.startsWith("COMMIT")) return {
|
|
257
|
+
op: "commit",
|
|
258
|
+
table: null
|
|
259
|
+
};
|
|
260
|
+
if (upper.startsWith("ROLLBACK")) return {
|
|
261
|
+
op: "rollback",
|
|
262
|
+
table: null
|
|
263
|
+
};
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Extract uppercase operation name for the db.operation attribute.
|
|
268
|
+
*/
|
|
269
|
+
function extractOperationName(sql) {
|
|
270
|
+
const trimmed = sql.trimStart();
|
|
271
|
+
const match = /^(\w+)/u.exec(trimmed);
|
|
272
|
+
return match ? match[1].toUpperCase() : null;
|
|
273
|
+
}
|
|
274
|
+
function matchTable(sql, pattern) {
|
|
275
|
+
const m = sql.match(pattern);
|
|
276
|
+
return m ? m[1] : null;
|
|
277
|
+
}
|
|
278
|
+
function formatError(error) {
|
|
279
|
+
if (error instanceof Error) return error.stack ?? `${error.name}: ${error.message}`;
|
|
280
|
+
return String(error);
|
|
281
|
+
}
|
|
282
|
+
function round(n) {
|
|
283
|
+
return Math.round(n * 100) / 100;
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
export { instrumentDrizzle, withSilgiCtx };
|
|
@@ -23,7 +23,11 @@ interface TraceSpan {
|
|
|
23
23
|
durationMs: number;
|
|
24
24
|
startOffsetMs?: number;
|
|
25
25
|
detail?: string;
|
|
26
|
+
input?: unknown;
|
|
27
|
+
output?: unknown;
|
|
26
28
|
error?: string;
|
|
29
|
+
/** Structured key-value attributes (db.name, auth.operation, user.id, etc.) */
|
|
30
|
+
attributes?: Record<string, string | number | boolean>;
|
|
27
31
|
}
|
|
28
32
|
interface ErrorEntry {
|
|
29
33
|
id: number;
|
|
@@ -110,11 +114,22 @@ interface AnalyticsSnapshot {
|
|
|
110
114
|
timeSeries: TimeWindow[];
|
|
111
115
|
}
|
|
112
116
|
declare class RequestTrace {
|
|
113
|
-
#private;
|
|
114
117
|
spans: TraceSpan[];
|
|
118
|
+
/** Procedure-level input — set via `setProcedureInput()` or `trace(..., { procedure: { input } })` */
|
|
119
|
+
procedureInput: unknown;
|
|
120
|
+
/** Procedure-level output — set via `setProcedureOutput()` or `trace(..., { procedure: { output } })` */
|
|
121
|
+
procedureOutput: unknown;
|
|
122
|
+
/** @internal Start time — used by integrations (drizzle etc.) for span offset calculation */
|
|
123
|
+
readonly t0: number;
|
|
115
124
|
trace<T>(name: string, fn: () => T | Promise<T>, opts?: {
|
|
116
125
|
kind?: SpanKind;
|
|
117
126
|
detail?: string;
|
|
127
|
+
input?: unknown;
|
|
128
|
+
output?: unknown | ((result: T) => unknown);
|
|
129
|
+
procedure?: {
|
|
130
|
+
input?: unknown;
|
|
131
|
+
output?: unknown | ((result: T) => unknown);
|
|
132
|
+
};
|
|
118
133
|
}): Promise<T>;
|
|
119
134
|
totalByKind(kind: SpanKind): number;
|
|
120
135
|
}
|
|
@@ -134,6 +149,12 @@ declare class RequestTrace {
|
|
|
134
149
|
declare function trace<T>(ctx: Record<string, unknown>, name: string, fn: () => T | Promise<T>, opts?: {
|
|
135
150
|
kind?: SpanKind;
|
|
136
151
|
detail?: string;
|
|
152
|
+
input?: unknown;
|
|
153
|
+
output?: unknown | ((result: T) => unknown);
|
|
154
|
+
procedure?: {
|
|
155
|
+
input?: unknown;
|
|
156
|
+
output?: unknown | ((result: T) => unknown);
|
|
157
|
+
};
|
|
137
158
|
}): Promise<T>;
|
|
138
159
|
declare class AnalyticsCollector {
|
|
139
160
|
#private;
|
|
@@ -65,7 +65,12 @@ function generateRequestId() {
|
|
|
65
65
|
}
|
|
66
66
|
var RequestTrace = class {
|
|
67
67
|
spans = [];
|
|
68
|
-
|
|
68
|
+
/** Procedure-level input — set via `setProcedureInput()` or `trace(..., { procedure: { input } })` */
|
|
69
|
+
procedureInput = void 0;
|
|
70
|
+
/** Procedure-level output — set via `setProcedureOutput()` or `trace(..., { procedure: { output } })` */
|
|
71
|
+
procedureOutput = void 0;
|
|
72
|
+
/** @internal Start time — used by integrations (drizzle etc.) for span offset calculation */
|
|
73
|
+
t0 = performance.now();
|
|
69
74
|
async trace(name, fn, opts) {
|
|
70
75
|
const start = performance.now();
|
|
71
76
|
const kind = opts?.kind ?? guessKind(name);
|
|
@@ -75,19 +80,28 @@ var RequestTrace = class {
|
|
|
75
80
|
name,
|
|
76
81
|
kind,
|
|
77
82
|
durationMs: round(performance.now() - start),
|
|
78
|
-
startOffsetMs: round(start - this
|
|
79
|
-
detail: opts?.detail
|
|
83
|
+
startOffsetMs: round(start - this.t0),
|
|
84
|
+
detail: opts?.detail,
|
|
85
|
+
input: opts?.input,
|
|
86
|
+
output: typeof opts?.output === "function" ? opts.output(result) : opts?.output
|
|
80
87
|
});
|
|
88
|
+
if (opts?.procedure) {
|
|
89
|
+
if (opts.procedure.input !== void 0) this.procedureInput = opts.procedure.input;
|
|
90
|
+
const po = opts.procedure.output;
|
|
91
|
+
if (po !== void 0) this.procedureOutput = typeof po === "function" ? po(result) : po;
|
|
92
|
+
}
|
|
81
93
|
return result;
|
|
82
94
|
} catch (err) {
|
|
83
95
|
this.spans.push({
|
|
84
96
|
name,
|
|
85
97
|
kind,
|
|
86
98
|
durationMs: round(performance.now() - start),
|
|
87
|
-
startOffsetMs: round(start - this
|
|
99
|
+
startOffsetMs: round(start - this.t0),
|
|
88
100
|
detail: opts?.detail,
|
|
101
|
+
input: opts?.input,
|
|
89
102
|
error: err instanceof Error ? err.message : String(err)
|
|
90
103
|
});
|
|
104
|
+
if (opts?.procedure?.input !== void 0) this.procedureInput = opts.procedure.input;
|
|
91
105
|
throw err;
|
|
92
106
|
}
|
|
93
107
|
}
|
|
@@ -277,7 +291,7 @@ var RequestAccumulator = class {
|
|
|
277
291
|
#collector;
|
|
278
292
|
constructor(request, collector) {
|
|
279
293
|
this.requestId = generateRequestId();
|
|
280
|
-
this
|
|
294
|
+
this.t0 = performance.now();
|
|
281
295
|
this.#request = request;
|
|
282
296
|
this.#collector = collector;
|
|
283
297
|
const existing = parseCookie(request.headers.get("cookie"), SESSION_COOKIE);
|
|
@@ -300,7 +314,7 @@ var RequestAccumulator = class {
|
|
|
300
314
|
/** Flush with response headers extracted from the actual Response object. */
|
|
301
315
|
flushWithResponse(res) {
|
|
302
316
|
if (this.#procedures.length === 0) return;
|
|
303
|
-
const durationMs = round(performance.now() - this
|
|
317
|
+
const durationMs = round(performance.now() - this.t0);
|
|
304
318
|
const headers = {};
|
|
305
319
|
this.#request.headers.forEach((v, k) => {
|
|
306
320
|
headers[k] = k === "authorization" || k === "cookie" ? "[REDACTED]" : v;
|