@flrande/bak-extension 0.6.11 → 0.6.13
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/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +588 -90
- package/dist/content.global.js +530 -12
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +233 -101
- package/dist/popup.html +260 -37
- package/package.json +2 -2
- package/public/popup.html +260 -37
- package/src/background.ts +344 -296
- package/src/content.ts +390 -22
- package/src/dynamic-data-tools.ts +790 -0
- package/src/popup.ts +291 -108
package/src/background.ts
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ConsoleEntry,
|
|
3
|
-
DebugDumpSection,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleEntry,
|
|
3
|
+
DebugDumpSection,
|
|
4
|
+
InspectFreshnessResult,
|
|
5
|
+
InspectLiveUpdatesResult,
|
|
6
|
+
InspectPageDataCandidateProbe,
|
|
7
|
+
InspectPageDataResult,
|
|
8
|
+
Locator,
|
|
9
|
+
NetworkEntry,
|
|
10
|
+
PageExecutionScope,
|
|
11
|
+
PageFetchResponse,
|
|
12
|
+
PageFrameResult,
|
|
13
|
+
PageFreshnessResult,
|
|
14
|
+
TableHandle,
|
|
15
|
+
TableSchema
|
|
16
|
+
} from '@flrande/bak-protocol';
|
|
17
|
+
import {
|
|
18
|
+
buildInspectPageDataResult,
|
|
19
|
+
buildPageDataProbe,
|
|
20
|
+
selectReplaySchemaMatch,
|
|
21
|
+
type InlineJsonInspectionSource,
|
|
22
|
+
type TableAnalysis
|
|
23
|
+
} from './dynamic-data-tools.js';
|
|
24
|
+
import {
|
|
25
|
+
clearNetworkEntries,
|
|
26
|
+
dropNetworkCapture,
|
|
27
|
+
ensureNetworkDebugger,
|
|
17
28
|
exportHar,
|
|
18
29
|
getNetworkEntry,
|
|
19
30
|
latestNetworkTimestamp,
|
|
@@ -65,15 +76,19 @@ interface RuntimeErrorDetails {
|
|
|
65
76
|
at: number;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
interface PopupSessionBindingSummary {
|
|
69
|
-
id: string;
|
|
70
|
-
label: string;
|
|
71
|
-
tabCount: number;
|
|
72
|
-
activeTabId: number | null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
interface PopupSessionBindingSummary {
|
|
80
|
+
id: string;
|
|
81
|
+
label: string;
|
|
82
|
+
tabCount: number;
|
|
83
|
+
activeTabId: number | null;
|
|
84
|
+
activeTabTitle: string | null;
|
|
85
|
+
activeTabUrl: string | null;
|
|
86
|
+
windowId: number | null;
|
|
87
|
+
groupId: number | null;
|
|
88
|
+
detached: boolean;
|
|
89
|
+
lastBindingUpdateAt: number | null;
|
|
90
|
+
lastBindingUpdateReason: string | null;
|
|
91
|
+
}
|
|
77
92
|
|
|
78
93
|
interface PopupState {
|
|
79
94
|
ok: true;
|
|
@@ -114,24 +129,48 @@ const CONTRACT_TIMESTAMP_CONTEXT_PATTERN =
|
|
|
114
129
|
/\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\b/i;
|
|
115
130
|
const EVENT_TIMESTAMP_CONTEXT_PATTERN = /\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\b/i;
|
|
116
131
|
|
|
117
|
-
interface TimestampEvidenceCandidate {
|
|
132
|
+
interface TimestampEvidenceCandidate {
|
|
118
133
|
value: string;
|
|
119
134
|
source: 'visible' | 'inline' | 'page-data' | 'network';
|
|
120
135
|
context?: string;
|
|
121
136
|
path?: string;
|
|
122
137
|
category?: 'data' | 'contract' | 'event' | 'unknown';
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface PageInspectionState {
|
|
141
|
+
url?: string;
|
|
142
|
+
title?: string;
|
|
143
|
+
html?: string;
|
|
144
|
+
visibleText?: Array<{ chunkId: string; text: string; sourceTag: string }>;
|
|
145
|
+
suspiciousGlobals?: string[];
|
|
146
|
+
globalsPreview?: string[];
|
|
147
|
+
visibleTimestamps?: string[];
|
|
148
|
+
visibleTimestampCandidates?: Array<Record<string, unknown>>;
|
|
149
|
+
inlineTimestamps?: string[];
|
|
150
|
+
inlineTimestampCandidates?: Array<Record<string, unknown>>;
|
|
151
|
+
tables?: TableHandle[];
|
|
152
|
+
inlineJsonSources?: InlineJsonInspectionSource[];
|
|
153
|
+
cookies?: Array<{ name: string }>;
|
|
154
|
+
lastMutationAt?: number;
|
|
155
|
+
pageLoadedAt?: number;
|
|
156
|
+
scripts?: {
|
|
157
|
+
inlineCount: number;
|
|
158
|
+
suspectedDataVars: string[];
|
|
159
|
+
};
|
|
160
|
+
storage?: {
|
|
161
|
+
localStorageKeys: string[];
|
|
162
|
+
sessionStorageKeys: string[];
|
|
163
|
+
};
|
|
164
|
+
frames?: Array<{ framePath: string[]; url: string }>;
|
|
165
|
+
context?: {
|
|
166
|
+
framePath: string[];
|
|
167
|
+
shadowPath: string[];
|
|
168
|
+
};
|
|
169
|
+
timers?: {
|
|
170
|
+
timeouts: number;
|
|
171
|
+
intervals: number;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
135
174
|
const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
|
|
136
175
|
'accept-encoding',
|
|
137
176
|
'authorization',
|
|
@@ -152,10 +191,11 @@ let nextReconnectAt: number | null = null;
|
|
|
152
191
|
let reconnectAttempt = 0;
|
|
153
192
|
let lastError: RuntimeErrorDetails | null = null;
|
|
154
193
|
let manualDisconnect = false;
|
|
155
|
-
let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
|
|
156
|
-
let preserveHumanFocusDepth = 0;
|
|
157
|
-
let lastBindingUpdateAt: number | null = null;
|
|
158
|
-
let lastBindingUpdateReason: string | null = null;
|
|
194
|
+
let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
|
|
195
|
+
let preserveHumanFocusDepth = 0;
|
|
196
|
+
let lastBindingUpdateAt: number | null = null;
|
|
197
|
+
let lastBindingUpdateReason: string | null = null;
|
|
198
|
+
const bindingUpdateMetadata = new Map<string, { at: number; reason: string }>();
|
|
159
199
|
|
|
160
200
|
async function getConfig(): Promise<ExtensionConfig> {
|
|
161
201
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
@@ -308,31 +348,40 @@ async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
|
308
348
|
return Object.values(await loadSessionBindingStateMap());
|
|
309
349
|
}
|
|
310
350
|
|
|
311
|
-
function summarizeSessionBindings(states: SessionBindingRecord[]): PopupState['sessionBindings'] {
|
|
312
|
-
const items =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
351
|
+
async function summarizeSessionBindings(states: SessionBindingRecord[]): Promise<PopupState['sessionBindings']> {
|
|
352
|
+
const items = await Promise.all(
|
|
353
|
+
states.map(async (state) => {
|
|
354
|
+
const detached = state.windowId === null || state.tabIds.length === 0;
|
|
355
|
+
const activeTab =
|
|
356
|
+
typeof state.activeTabId === 'number' ? await sessionBindingBrowser.getTab(state.activeTabId) : null;
|
|
357
|
+
const bindingUpdate = bindingUpdateMetadata.get(state.id);
|
|
358
|
+
return {
|
|
359
|
+
id: state.id,
|
|
360
|
+
label: state.label,
|
|
361
|
+
tabCount: state.tabIds.length,
|
|
362
|
+
activeTabId: state.activeTabId,
|
|
363
|
+
activeTabTitle: activeTab?.title ?? null,
|
|
364
|
+
activeTabUrl: activeTab?.url ?? null,
|
|
365
|
+
windowId: state.windowId,
|
|
366
|
+
groupId: state.groupId,
|
|
367
|
+
detached,
|
|
368
|
+
lastBindingUpdateAt: bindingUpdate?.at ?? null,
|
|
369
|
+
lastBindingUpdateReason: bindingUpdate?.reason ?? null
|
|
370
|
+
} satisfies PopupSessionBindingSummary;
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
return {
|
|
374
|
+
count: items.length,
|
|
375
|
+
attachedCount: items.filter((item) => !item.detached).length,
|
|
327
376
|
detachedCount: items.filter((item) => item.detached).length,
|
|
328
377
|
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
329
378
|
items
|
|
330
379
|
};
|
|
331
380
|
}
|
|
332
381
|
|
|
333
|
-
async function buildPopupState(): Promise<PopupState> {
|
|
334
|
-
const config = await getConfig();
|
|
335
|
-
const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
|
|
382
|
+
async function buildPopupState(): Promise<PopupState> {
|
|
383
|
+
const config = await getConfig();
|
|
384
|
+
const sessionBindings = await summarizeSessionBindings(await listSessionBindingStates());
|
|
336
385
|
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
337
386
|
let connectionState: PopupState['connectionState'];
|
|
338
387
|
if (!config.token) {
|
|
@@ -395,16 +444,20 @@ function toSessionBindingEventBrowser(state: SessionBindingRecord | null): Recor
|
|
|
395
444
|
};
|
|
396
445
|
}
|
|
397
446
|
|
|
398
|
-
function emitSessionBindingUpdated(
|
|
399
|
-
bindingId: string,
|
|
400
|
-
reason: string,
|
|
401
|
-
state: SessionBindingRecord | null,
|
|
402
|
-
extras: Record<string, unknown> = {}
|
|
403
|
-
): void {
|
|
404
|
-
lastBindingUpdateAt = Date.now();
|
|
405
|
-
lastBindingUpdateReason = reason;
|
|
406
|
-
|
|
407
|
-
|
|
447
|
+
function emitSessionBindingUpdated(
|
|
448
|
+
bindingId: string,
|
|
449
|
+
reason: string,
|
|
450
|
+
state: SessionBindingRecord | null,
|
|
451
|
+
extras: Record<string, unknown> = {}
|
|
452
|
+
): void {
|
|
453
|
+
lastBindingUpdateAt = Date.now();
|
|
454
|
+
lastBindingUpdateReason = reason;
|
|
455
|
+
bindingUpdateMetadata.set(bindingId, {
|
|
456
|
+
at: lastBindingUpdateAt,
|
|
457
|
+
reason
|
|
458
|
+
});
|
|
459
|
+
sendEvent('sessionBinding.updated', {
|
|
460
|
+
bindingId,
|
|
408
461
|
reason,
|
|
409
462
|
browser: toSessionBindingEventBrowser(state),
|
|
410
463
|
...extras
|
|
@@ -1391,118 +1444,91 @@ function computeFreshnessAssessment(input: {
|
|
|
1391
1444
|
return 'unknown';
|
|
1392
1445
|
}
|
|
1393
1446
|
|
|
1394
|
-
async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<
|
|
1395
|
-
return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
async function probePageDataCandidatesForTab(tabId: number, inspection:
|
|
1399
|
-
const candidateNames = [
|
|
1400
|
-
...(Array.isArray(inspection.suspiciousGlobals) ? inspection.suspiciousGlobals.map(String) : []),
|
|
1401
|
-
...(Array.isArray(inspection.globalsPreview) ? inspection.globalsPreview.map(String) : [])
|
|
1402
|
-
]
|
|
1403
|
-
.filter((name, index, array) => /^[A-Za-z_$][\w$]*$/.test(name) && array.indexOf(name) === index)
|
|
1404
|
-
.slice(0, 16);
|
|
1447
|
+
async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<PageInspectionState> {
|
|
1448
|
+
return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as PageInspectionState;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function probePageDataCandidatesForTab(tabId: number, inspection: PageInspectionState): Promise<InspectPageDataCandidateProbe[]> {
|
|
1452
|
+
const candidateNames = [
|
|
1453
|
+
...(Array.isArray(inspection.suspiciousGlobals) ? inspection.suspiciousGlobals.map(String) : []),
|
|
1454
|
+
...(Array.isArray(inspection.globalsPreview) ? inspection.globalsPreview.map(String) : [])
|
|
1455
|
+
]
|
|
1456
|
+
.filter((name, index, array) => /^[A-Za-z_$][\w$]*$/.test(name) && array.indexOf(name) === index)
|
|
1457
|
+
.slice(0, 16);
|
|
1405
1458
|
if (candidateNames.length === 0) {
|
|
1406
1459
|
return [];
|
|
1407
1460
|
}
|
|
1408
1461
|
|
|
1409
|
-
const expr = `(() => {
|
|
1410
|
-
const candidates = ${JSON.stringify(candidateNames)};
|
|
1411
|
-
const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
|
|
1412
|
-
const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
|
|
1413
|
-
const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
if (
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
return sampled;
|
|
1445
|
-
};
|
|
1446
|
-
const collectTimestamps = (value, path, depth, collected) => {
|
|
1447
|
-
if (collected.length >= 16) return;
|
|
1448
|
-
if (isTimestampString(value)) {
|
|
1449
|
-
collected.push({ path, value: String(value), category: classify(path, value) });
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
if (depth >= 3) return;
|
|
1453
|
-
if (Array.isArray(value)) {
|
|
1454
|
-
value.slice(0, 3).forEach((item, index) => collectTimestamps(item, path + '[' + index + ']', depth + 1, collected));
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
if (value && typeof value === 'object') {
|
|
1458
|
-
Object.keys(value)
|
|
1459
|
-
.slice(0, 8)
|
|
1460
|
-
.forEach((key) => {
|
|
1461
|
-
try {
|
|
1462
|
-
collectTimestamps(value[key], path ? path + '.' + key : key, depth + 1, collected);
|
|
1463
|
-
} catch {
|
|
1464
|
-
// Ignore unreadable nested properties.
|
|
1465
|
-
}
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
const readCandidate = (name) => {
|
|
1470
|
-
if (name in globalThis) {
|
|
1471
|
-
return { resolver: 'globalThis', value: globalThis[name] };
|
|
1472
|
-
}
|
|
1473
|
-
return { resolver: 'lexical', value: globalThis.eval(name) };
|
|
1462
|
+
const expr = `(() => {
|
|
1463
|
+
const candidates = ${JSON.stringify(candidateNames)};
|
|
1464
|
+
const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
|
|
1465
|
+
const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
|
|
1466
|
+
const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
|
|
1467
|
+
const sampleValue = (value, depth = 0) => {
|
|
1468
|
+
if (depth >= 2 || value == null || typeof value !== 'object') {
|
|
1469
|
+
if (typeof value === 'string') {
|
|
1470
|
+
return value.length > 160 ? value.slice(0, 160) : value;
|
|
1471
|
+
}
|
|
1472
|
+
if (typeof value === 'function') {
|
|
1473
|
+
return '[Function]';
|
|
1474
|
+
}
|
|
1475
|
+
return value;
|
|
1476
|
+
}
|
|
1477
|
+
if (Array.isArray(value)) {
|
|
1478
|
+
return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
|
|
1479
|
+
}
|
|
1480
|
+
const sampled = {};
|
|
1481
|
+
for (const key of Object.keys(value).slice(0, 8)) {
|
|
1482
|
+
try {
|
|
1483
|
+
sampled[key] = sampleValue(value[key], depth + 1);
|
|
1484
|
+
} catch {
|
|
1485
|
+
sampled[key] = '[Unreadable]';
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return sampled;
|
|
1489
|
+
};
|
|
1490
|
+
const readCandidate = (name) => {
|
|
1491
|
+
if (name in globalThis) {
|
|
1492
|
+
return { resolver: 'globalThis', value: globalThis[name] };
|
|
1493
|
+
}
|
|
1494
|
+
return { resolver: 'lexical', value: globalThis.eval(name) };
|
|
1474
1495
|
};
|
|
1475
1496
|
const results = [];
|
|
1476
|
-
for (const name of candidates) {
|
|
1477
|
-
try {
|
|
1478
|
-
const resolved = readCandidate(name);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
} catch {
|
|
1488
|
-
// Ignore inaccessible candidates.
|
|
1489
|
-
}
|
|
1497
|
+
for (const name of candidates) {
|
|
1498
|
+
try {
|
|
1499
|
+
const resolved = readCandidate(name);
|
|
1500
|
+
results.push({
|
|
1501
|
+
name,
|
|
1502
|
+
resolver: resolved.resolver,
|
|
1503
|
+
sample: sampleValue(resolved.value)
|
|
1504
|
+
});
|
|
1505
|
+
} catch {
|
|
1506
|
+
// Ignore inaccessible candidates.
|
|
1507
|
+
}
|
|
1490
1508
|
}
|
|
1491
1509
|
return results;
|
|
1492
1510
|
})()`;
|
|
1493
|
-
|
|
1494
|
-
try {
|
|
1495
|
-
const evaluated = await executePageWorld<
|
|
1496
|
-
expr,
|
|
1497
|
-
scope: 'current',
|
|
1498
|
-
maxBytes: 64 * 1024
|
|
1499
|
-
});
|
|
1500
|
-
const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
const evaluated = await executePageWorld<Array<{ name: string; resolver: 'globalThis' | 'lexical'; sample: unknown }>>(tabId, 'eval', {
|
|
1514
|
+
expr,
|
|
1515
|
+
scope: 'current',
|
|
1516
|
+
maxBytes: 64 * 1024
|
|
1517
|
+
});
|
|
1518
|
+
const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
|
|
1519
|
+
if (!Array.isArray(frameResult?.value)) {
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
return frameResult.value
|
|
1523
|
+
.filter(
|
|
1524
|
+
(candidate): candidate is { name: string; resolver: 'globalThis' | 'lexical'; sample: unknown } =>
|
|
1525
|
+
typeof candidate === 'object' && candidate !== null && typeof candidate.name === 'string'
|
|
1526
|
+
)
|
|
1527
|
+
.map((candidate) => buildPageDataProbe(candidate.name, candidate.resolver, candidate.sample));
|
|
1528
|
+
} catch {
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1506
1532
|
|
|
1507
1533
|
async function buildFreshnessForTab(tabId: number, params: Record<string, unknown> = {}): Promise<PageFreshnessResult> {
|
|
1508
1534
|
const inspection = await collectPageInspection(tabId, params);
|
|
@@ -1598,7 +1624,7 @@ async function buildFreshnessForTab(tabId: number, params: Record<string, unknow
|
|
|
1598
1624
|
};
|
|
1599
1625
|
}
|
|
1600
1626
|
|
|
1601
|
-
function summarizeNetworkCadence(entries: NetworkEntry[]):
|
|
1627
|
+
function summarizeNetworkCadence(entries: NetworkEntry[]): InspectLiveUpdatesResult['networkCadence'] {
|
|
1602
1628
|
const relevant = entries
|
|
1603
1629
|
.filter((entry) => entry.kind === 'fetch' || entry.kind === 'xhr')
|
|
1604
1630
|
.slice()
|
|
@@ -1639,10 +1665,10 @@ function summarizeNetworkCadence(entries: NetworkEntry[]): Record<string, unknow
|
|
|
1639
1665
|
};
|
|
1640
1666
|
}
|
|
1641
1667
|
|
|
1642
|
-
function extractReplayRowsCandidate(json: unknown): { rows: unknown[]; source: string } | null {
|
|
1643
|
-
if (Array.isArray(json)) {
|
|
1644
|
-
return { rows: json, source: '$' };
|
|
1645
|
-
}
|
|
1668
|
+
function extractReplayRowsCandidate(json: unknown): { rows: unknown[]; source: string } | null {
|
|
1669
|
+
if (Array.isArray(json)) {
|
|
1670
|
+
return { rows: json, source: '$' };
|
|
1671
|
+
}
|
|
1646
1672
|
if (typeof json !== 'object' || json === null) {
|
|
1647
1673
|
return null;
|
|
1648
1674
|
}
|
|
@@ -1652,71 +1678,77 @@ function extractReplayRowsCandidate(json: unknown): { rows: unknown[]; source: s
|
|
|
1652
1678
|
if (Array.isArray(record[key])) {
|
|
1653
1679
|
return { rows: record[key] as unknown[], source: `$.${key}` };
|
|
1654
1680
|
}
|
|
1655
|
-
}
|
|
1656
|
-
return null;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
async function
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1681
|
+
}
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function collectTableAnalyses(tabId: number): Promise<TableAnalysis[]> {
|
|
1686
|
+
const tablesResult = (await forwardContentRpc(tabId, 'table.list', {})) as { tables?: TableHandle[] };
|
|
1687
|
+
const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
|
|
1688
|
+
const analyses: TableAnalysis[] = [];
|
|
1689
|
+
for (const table of tables) {
|
|
1690
|
+
const schemaResult = (await forwardContentRpc(tabId, 'table.schema', { table: table.id })) as {
|
|
1691
|
+
table?: TableHandle;
|
|
1692
|
+
schema?: TableSchema;
|
|
1693
|
+
};
|
|
1694
|
+
const rowsResult = (await forwardContentRpc(tabId, 'table.rows', {
|
|
1695
|
+
table: table.id,
|
|
1696
|
+
limit: 8
|
|
1697
|
+
})) as {
|
|
1698
|
+
table?: TableHandle;
|
|
1699
|
+
rows?: Array<Record<string, unknown>>;
|
|
1700
|
+
};
|
|
1701
|
+
if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
|
|
1702
|
+
analyses.push({
|
|
1703
|
+
table: schemaResult.table ?? rowsResult.table ?? table,
|
|
1704
|
+
schema: schemaResult.schema,
|
|
1705
|
+
sampleRows: Array.isArray(rowsResult.rows) ? rowsResult.rows.slice(0, 8) : []
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return analyses;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
async function enrichReplayWithSchema(tabId: number, requestId: string, response: PageFetchResponse): Promise<PageFetchResponse> {
|
|
1713
|
+
const candidate = extractReplayRowsCandidate(response.json);
|
|
1714
|
+
if (!candidate || candidate.rows.length === 0) {
|
|
1715
|
+
return response;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const tables = await collectTableAnalyses(tabId);
|
|
1719
|
+
if (tables.length === 0) {
|
|
1720
|
+
return response;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const inspection = await collectPageInspection(tabId, {});
|
|
1724
|
+
const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
|
|
1725
|
+
const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
|
|
1726
|
+
const pageDataReport = buildInspectPageDataResult({
|
|
1727
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
1728
|
+
tables: inspection.tables ?? [],
|
|
1729
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
1730
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
1731
|
+
pageDataCandidates,
|
|
1732
|
+
recentNetwork,
|
|
1733
|
+
tableAnalyses: tables,
|
|
1734
|
+
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
|
|
1735
|
+
});
|
|
1736
|
+
const matched = selectReplaySchemaMatch(response.json, tables, {
|
|
1737
|
+
preferredSourceId: `networkResponse:${requestId}`,
|
|
1738
|
+
mappings: pageDataReport.sourceMappings
|
|
1739
|
+
});
|
|
1740
|
+
if (matched) {
|
|
1741
|
+
return {
|
|
1742
|
+
...response,
|
|
1743
|
+
table: matched.table,
|
|
1744
|
+
schema: matched.schema,
|
|
1745
|
+
mappedRows: matched.mappedRows,
|
|
1746
|
+
mappingSource: matched.mappingSource
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
return response;
|
|
1751
|
+
}
|
|
1720
1752
|
|
|
1721
1753
|
async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
1722
1754
|
const params = request.params ?? {};
|
|
@@ -2241,9 +2273,11 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2241
2273
|
if (!first) {
|
|
2242
2274
|
throw toError('E_EXECUTION', 'network replay returned no response payload');
|
|
2243
2275
|
}
|
|
2244
|
-
return params.withSchema === 'auto' && params.mode === 'json'
|
|
2245
|
-
|
|
2246
|
-
|
|
2276
|
+
return params.withSchema === 'auto' && params.mode === 'json'
|
|
2277
|
+
? await enrichReplayWithSchema(tab.id!, String(params.id ?? ''), first)
|
|
2278
|
+
: first;
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2247
2281
|
case 'page.freshness': {
|
|
2248
2282
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2249
2283
|
const tab = await withTab(target);
|
|
@@ -2312,61 +2346,75 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
2312
2346
|
return filtered;
|
|
2313
2347
|
});
|
|
2314
2348
|
}
|
|
2315
|
-
case 'inspect.pageData': {
|
|
2316
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2317
|
-
const tab = await withTab(target);
|
|
2318
|
-
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
2319
|
-
const inspection = await collectPageInspection(tab.id!, params);
|
|
2320
|
-
const pageDataCandidates = await probePageDataCandidatesForTab(tab.id!, inspection);
|
|
2321
|
-
const network = listNetworkEntries(tab.id!, { limit: 10 });
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2349
|
+
case 'inspect.pageData': {
|
|
2350
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2351
|
+
const tab = await withTab(target);
|
|
2352
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
2353
|
+
const inspection = await collectPageInspection(tab.id!, params);
|
|
2354
|
+
const pageDataCandidates = await probePageDataCandidatesForTab(tab.id!, inspection);
|
|
2355
|
+
const network = listNetworkEntries(tab.id!, { limit: 10 });
|
|
2356
|
+
const tableAnalyses = await collectTableAnalyses(tab.id!);
|
|
2357
|
+
const enriched = buildInspectPageDataResult({
|
|
2358
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
2359
|
+
tables: inspection.tables ?? [],
|
|
2360
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
2361
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
2362
|
+
pageDataCandidates,
|
|
2363
|
+
recentNetwork: network,
|
|
2364
|
+
tableAnalyses,
|
|
2365
|
+
inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
|
|
2366
|
+
});
|
|
2367
|
+
const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
|
|
2368
|
+
return {
|
|
2369
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
2370
|
+
tables: inspection.tables ?? [],
|
|
2371
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
2372
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
2373
|
+
pageDataCandidates,
|
|
2374
|
+
recentNetwork: network,
|
|
2375
|
+
dataSources: enriched.dataSources,
|
|
2376
|
+
sourceMappings: enriched.sourceMappings,
|
|
2377
|
+
recommendedNextActions: enriched.recommendedNextActions,
|
|
2378
|
+
recommendedNextSteps: [
|
|
2379
|
+
...recommendedNextSteps,
|
|
2380
|
+
...['bak page freshness'].filter((command) => !recommendedNextSteps.includes(command))
|
|
2381
|
+
]
|
|
2382
|
+
} satisfies InspectPageDataResult;
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
case 'inspect.liveUpdates': {
|
|
2386
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2387
|
+
const tab = await withTab(target);
|
|
2388
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
2389
|
+
const inspection = await collectPageInspection(tab.id!, params);
|
|
2390
|
+
const network = listNetworkEntries(tab.id!, { limit: 25 });
|
|
2391
|
+
return {
|
|
2392
|
+
lastMutationAt: inspection.lastMutationAt ?? null,
|
|
2393
|
+
timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
|
|
2394
|
+
networkCount: network.length,
|
|
2395
|
+
networkCadence: summarizeNetworkCadence(network),
|
|
2396
|
+
recentNetwork: network.slice(0, 10)
|
|
2397
|
+
} satisfies InspectLiveUpdatesResult;
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
case 'inspect.freshness': {
|
|
2401
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2402
|
+
const tab = await withTab(target);
|
|
2403
|
+
const freshness = await buildFreshnessForTab(tab.id!, params);
|
|
2404
|
+
return {
|
|
2357
2405
|
...freshness,
|
|
2358
2406
|
lagMs:
|
|
2359
2407
|
typeof freshness.latestNetworkTimestamp === 'number' &&
|
|
2360
2408
|
typeof (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp) === 'number'
|
|
2361
2409
|
? Math.max(
|
|
2362
2410
|
0,
|
|
2363
|
-
freshness.latestNetworkTimestamp -
|
|
2364
|
-
(freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp ?? freshness.latestNetworkTimestamp)
|
|
2365
|
-
)
|
|
2366
|
-
: null
|
|
2367
|
-
};
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2411
|
+
freshness.latestNetworkTimestamp -
|
|
2412
|
+
(freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp ?? freshness.latestNetworkTimestamp)
|
|
2413
|
+
)
|
|
2414
|
+
: null
|
|
2415
|
+
} satisfies InspectFreshnessResult;
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2370
2418
|
case 'capture.snapshot': {
|
|
2371
2419
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
2372
2420
|
const tab = await withTab(target);
|