@flrande/bak-extension 0.6.15 → 0.6.17
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 +898 -112
- package/dist/content.global.js +365 -23
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/background.ts +865 -273
- package/src/content.ts +266 -27
- package/src/dynamic-data-tools.ts +141 -7
- package/src/network-debugger.ts +106 -56
- package/src/network-tools.ts +184 -0
package/src/content.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type {
|
|
|
2
2
|
AccessibilityNode,
|
|
3
3
|
ConsoleEntry,
|
|
4
4
|
ElementMapItem,
|
|
5
|
+
InspectPageDateControl,
|
|
6
|
+
InspectPageModeGroup,
|
|
7
|
+
InspectPageModeOption,
|
|
5
8
|
Locator,
|
|
6
9
|
NetworkEntry,
|
|
7
10
|
PageDomSummary,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
sampleValue,
|
|
26
29
|
type InlineJsonInspectionSource
|
|
27
30
|
} from './dynamic-data-tools.js';
|
|
31
|
+
import { buildNetworkEntryDerivedFields, clampNetworkListLimit, networkEntryMatchesFilters } from './network-tools.js';
|
|
28
32
|
import { inferSafeName, redactElementText, redactHtmlSnapshot, type RedactTextOptions } from './privacy.js';
|
|
29
33
|
import { unsupportedLocatorHint } from './limitations.js';
|
|
30
34
|
|
|
@@ -1619,30 +1623,22 @@ function networkSnapshotEntries(): NetworkEntry[] {
|
|
|
1619
1623
|
}
|
|
1620
1624
|
|
|
1621
1625
|
function filterNetworkEntries(params: Record<string, unknown>): NetworkEntry[] {
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
if (typeof status === 'number' && entry.status !== status) {
|
|
1640
|
-
return false;
|
|
1641
|
-
}
|
|
1642
|
-
return true;
|
|
1643
|
-
})
|
|
1644
|
-
.slice(-limit)
|
|
1645
|
-
.reverse();
|
|
1626
|
+
const limit = clampNetworkListLimit(typeof params.limit === 'number' ? params.limit : undefined, 50);
|
|
1627
|
+
const ordered = networkSnapshotEntries()
|
|
1628
|
+
.map((entry) => ({ ...entry, ...buildNetworkEntryDerivedFields(entry) }))
|
|
1629
|
+
.filter((entry) =>
|
|
1630
|
+
networkEntryMatchesFilters(entry, {
|
|
1631
|
+
urlIncludes: typeof params.urlIncludes === 'string' ? params.urlIncludes : undefined,
|
|
1632
|
+
status: typeof params.status === 'number' ? params.status : undefined,
|
|
1633
|
+
method: typeof params.method === 'string' ? params.method : undefined,
|
|
1634
|
+
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
1635
|
+
resourceType: typeof params.resourceType === 'string' ? params.resourceType : undefined,
|
|
1636
|
+
kind: typeof params.kind === 'string' ? (params.kind as NetworkEntry['kind']) : undefined,
|
|
1637
|
+
sinceTs: typeof params.sinceTs === 'number' ? params.sinceTs : undefined
|
|
1638
|
+
})
|
|
1639
|
+
)
|
|
1640
|
+
.slice(-limit);
|
|
1641
|
+
return params.tail === true ? ordered : ordered.reverse();
|
|
1646
1642
|
}
|
|
1647
1643
|
|
|
1648
1644
|
function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): NetworkEntry {
|
|
@@ -1662,12 +1658,26 @@ function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): Netw
|
|
|
1662
1658
|
delete clone.requestHeaders;
|
|
1663
1659
|
delete clone.requestBodyPreview;
|
|
1664
1660
|
delete clone.requestBodyTruncated;
|
|
1661
|
+
if (clone.preview) {
|
|
1662
|
+
clone.preview = { ...clone.preview };
|
|
1663
|
+
delete clone.preview.request;
|
|
1664
|
+
if (!clone.preview.query && !clone.preview.request && !clone.preview.response) {
|
|
1665
|
+
delete clone.preview;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1665
1668
|
}
|
|
1666
1669
|
if (!sections.has('response')) {
|
|
1667
1670
|
delete clone.responseHeaders;
|
|
1668
1671
|
delete clone.responseBodyPreview;
|
|
1669
1672
|
delete clone.responseBodyTruncated;
|
|
1670
1673
|
delete clone.binary;
|
|
1674
|
+
if (clone.preview) {
|
|
1675
|
+
clone.preview = { ...clone.preview };
|
|
1676
|
+
delete clone.preview.response;
|
|
1677
|
+
if (!clone.preview.query && !clone.preview.request && !clone.preview.response) {
|
|
1678
|
+
delete clone.preview;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1671
1681
|
}
|
|
1672
1682
|
return clone;
|
|
1673
1683
|
}
|
|
@@ -1936,7 +1946,7 @@ function describeTables(): TableHandle[] {
|
|
|
1936
1946
|
for (const [index, table] of htmlTables.entries()) {
|
|
1937
1947
|
const handle: TableHandle = {
|
|
1938
1948
|
id: buildTableId(table.closest('.dataTables_wrapper') ? 'dataTables' : 'html', index),
|
|
1939
|
-
|
|
1949
|
+
label: (table.getAttribute('aria-label') || table.getAttribute('data-testid') || table.id || `table-${index + 1}`).trim(),
|
|
1940
1950
|
kind: table.closest('.dataTables_wrapper') ? 'dataTables' : 'html',
|
|
1941
1951
|
selector: table.id ? `#${table.id}` : undefined,
|
|
1942
1952
|
rowCount: table.querySelectorAll('tbody tr').length || table.querySelectorAll('tr').length,
|
|
@@ -1949,7 +1959,7 @@ function describeTables(): TableHandle[] {
|
|
|
1949
1959
|
const kind: TableHandle['kind'] = grid.className.includes('ag-') ? 'ag-grid' : 'aria-grid';
|
|
1950
1960
|
const handle: TableHandle = {
|
|
1951
1961
|
id: buildTableId(kind, index),
|
|
1952
|
-
|
|
1962
|
+
label: (grid.getAttribute('aria-label') || grid.getAttribute('data-testid') || grid.id || `grid-${index + 1}`).trim(),
|
|
1953
1963
|
kind,
|
|
1954
1964
|
selector: grid.id ? `#${grid.id}` : undefined,
|
|
1955
1965
|
rowCount: gridRowNodes(grid).length,
|
|
@@ -1962,7 +1972,13 @@ function describeTables(): TableHandle[] {
|
|
|
1962
1972
|
|
|
1963
1973
|
function resolveTable(handleId: string): { table: TableHandle; element: Element | null } | null {
|
|
1964
1974
|
const tables = describeTables();
|
|
1965
|
-
const
|
|
1975
|
+
const normalizedHandleId = handleId.trim().toLowerCase();
|
|
1976
|
+
const handle = tables.find((candidate) => {
|
|
1977
|
+
if (candidate.id === handleId) {
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
return typeof candidate.label === 'string' && candidate.label.trim().toLowerCase() === normalizedHandleId;
|
|
1981
|
+
});
|
|
1966
1982
|
if (!handle) {
|
|
1967
1983
|
return null;
|
|
1968
1984
|
}
|
|
@@ -2635,6 +2651,225 @@ function collectInlineJsonSources(root: ParentNode): InlineJsonInspectionSource[
|
|
|
2635
2651
|
.filter((item): item is InlineJsonInspectionSource => item !== null);
|
|
2636
2652
|
}
|
|
2637
2653
|
|
|
2654
|
+
const MODE_OPTION_PATTERN = /\b(latest|historical|history|archive|archived|live|intraday|today|yesterday|session|completed)\b/i;
|
|
2655
|
+
|
|
2656
|
+
function selectorHintForElement(element: Element): string {
|
|
2657
|
+
if (element instanceof HTMLElement && element.id) {
|
|
2658
|
+
return `#${element.id}`;
|
|
2659
|
+
}
|
|
2660
|
+
if (element instanceof HTMLElement && element.getAttribute('name')) {
|
|
2661
|
+
return `${element.tagName.toLowerCase()}[name="${element.getAttribute('name')}"]`;
|
|
2662
|
+
}
|
|
2663
|
+
const role = element.getAttribute('role');
|
|
2664
|
+
if (role) {
|
|
2665
|
+
return `${element.tagName.toLowerCase()}[role="${role}"]`;
|
|
2666
|
+
}
|
|
2667
|
+
return element.tagName.toLowerCase();
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function cleanControlText(value: string | null | undefined): string | undefined {
|
|
2671
|
+
if (typeof value !== 'string') {
|
|
2672
|
+
return undefined;
|
|
2673
|
+
}
|
|
2674
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
2675
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
function extractElementLabel(element: Element): string | undefined {
|
|
2679
|
+
if (element instanceof HTMLInputElement && element.labels && element.labels.length > 0) {
|
|
2680
|
+
return cleanControlText(element.labels[0]?.textContent);
|
|
2681
|
+
}
|
|
2682
|
+
const ariaLabel = cleanControlText(element.getAttribute('aria-label'));
|
|
2683
|
+
if (ariaLabel) {
|
|
2684
|
+
return ariaLabel;
|
|
2685
|
+
}
|
|
2686
|
+
if (element instanceof HTMLElement && element.id) {
|
|
2687
|
+
const label = document.querySelector(`label[for="${CSS.escape(element.id)}"]`);
|
|
2688
|
+
if (label) {
|
|
2689
|
+
return cleanControlText(label.textContent);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
const fieldset = element.closest('fieldset');
|
|
2693
|
+
if (fieldset) {
|
|
2694
|
+
const legend = fieldset.querySelector('legend');
|
|
2695
|
+
const legendText = cleanControlText(legend?.textContent);
|
|
2696
|
+
if (legendText) {
|
|
2697
|
+
return legendText;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return cleanControlText(element.parentElement?.getAttribute('aria-label') ?? element.parentElement?.getAttribute('data-label'));
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
function inferModeOption(element: Element): InspectPageModeOption | null {
|
|
2704
|
+
const label =
|
|
2705
|
+
cleanControlText(
|
|
2706
|
+
element instanceof HTMLInputElement && element.type === 'radio'
|
|
2707
|
+
? element.labels?.[0]?.textContent ?? element.value
|
|
2708
|
+
: element instanceof HTMLOptionElement
|
|
2709
|
+
? element.textContent ?? element.value
|
|
2710
|
+
: element.textContent ?? element.getAttribute('value') ?? element.getAttribute('aria-label')
|
|
2711
|
+
) ?? cleanControlText(element.getAttribute('value'));
|
|
2712
|
+
if (!label || !MODE_OPTION_PATTERN.test(label)) {
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
const value =
|
|
2716
|
+
cleanControlText(
|
|
2717
|
+
element instanceof HTMLInputElement || element instanceof HTMLOptionElement
|
|
2718
|
+
? element.value
|
|
2719
|
+
: element.getAttribute('value') ?? label
|
|
2720
|
+
) ?? label;
|
|
2721
|
+
const selected =
|
|
2722
|
+
(element instanceof HTMLOptionElement && element.selected) ||
|
|
2723
|
+
(element instanceof HTMLInputElement && element.checked) ||
|
|
2724
|
+
element.getAttribute('aria-selected') === 'true' ||
|
|
2725
|
+
element.getAttribute('aria-pressed') === 'true' ||
|
|
2726
|
+
element.classList.contains('active') ||
|
|
2727
|
+
element.classList.contains('selected') ||
|
|
2728
|
+
element.classList.contains('current');
|
|
2729
|
+
return {
|
|
2730
|
+
label,
|
|
2731
|
+
value,
|
|
2732
|
+
selected
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function buildModeGroup(
|
|
2737
|
+
elements: Element[],
|
|
2738
|
+
controlType: InspectPageModeGroup['controlType'],
|
|
2739
|
+
container?: Element | null
|
|
2740
|
+
): InspectPageModeGroup | null {
|
|
2741
|
+
const options = elements.map((element) => inferModeOption(element)).filter((item): item is InspectPageModeOption => item !== null);
|
|
2742
|
+
const deduped = options.filter((option, index, array) => array.findIndex((candidate) => candidate.label === option.label) === index);
|
|
2743
|
+
if (deduped.length < 2) {
|
|
2744
|
+
return null;
|
|
2745
|
+
}
|
|
2746
|
+
return {
|
|
2747
|
+
controlType,
|
|
2748
|
+
label: container ? extractElementLabel(container) : undefined,
|
|
2749
|
+
selectorHint: container ? selectorHintForElement(container) : selectorHintForElement(elements[0]!),
|
|
2750
|
+
options: deduped
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function collectModeGroups(root: ParentNode): InspectPageModeGroup[] {
|
|
2755
|
+
const groups: InspectPageModeGroup[] = [];
|
|
2756
|
+
const handled = new Set<Element>();
|
|
2757
|
+
|
|
2758
|
+
for (const select of Array.from(root.querySelectorAll('select'))) {
|
|
2759
|
+
const options = Array.from(select.querySelectorAll('option'));
|
|
2760
|
+
const group = buildModeGroup(options, 'select', select);
|
|
2761
|
+
if (!group) {
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2764
|
+
options.forEach((option) => handled.add(option));
|
|
2765
|
+
groups.push(group);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
for (const container of Array.from(root.querySelectorAll('[role="tablist"], [role="radiogroup"], fieldset'))) {
|
|
2769
|
+
const role = container.getAttribute('role');
|
|
2770
|
+
const elements =
|
|
2771
|
+
role === 'tablist'
|
|
2772
|
+
? Array.from(container.querySelectorAll('[role="tab"]'))
|
|
2773
|
+
: role === 'radiogroup'
|
|
2774
|
+
? Array.from(container.querySelectorAll('[role="radio"], input[type="radio"]'))
|
|
2775
|
+
: Array.from(container.querySelectorAll('input[type="radio"], button'));
|
|
2776
|
+
const untouched = elements.filter((element) => !handled.has(element));
|
|
2777
|
+
const group = buildModeGroup(untouched, role === 'tablist' ? 'tabs' : role === 'radiogroup' ? 'radio' : 'buttons', container);
|
|
2778
|
+
if (!group) {
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
untouched.forEach((element) => handled.add(element));
|
|
2782
|
+
groups.push(group);
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
const candidateParents = new Set<HTMLElement>();
|
|
2786
|
+
for (const button of Array.from(root.querySelectorAll('button'))) {
|
|
2787
|
+
if (button.parentElement) {
|
|
2788
|
+
candidateParents.add(button.parentElement);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
for (const parent of candidateParents) {
|
|
2792
|
+
const directButtons = Array.from(parent.children).filter((child): child is HTMLButtonElement => child instanceof HTMLButtonElement);
|
|
2793
|
+
if (directButtons.length < 2 || directButtons.length > 6 || directButtons.every((button) => handled.has(button))) {
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
const group = buildModeGroup(
|
|
2797
|
+
directButtons.filter((button) => !handled.has(button)),
|
|
2798
|
+
'buttons',
|
|
2799
|
+
parent
|
|
2800
|
+
);
|
|
2801
|
+
if (!group) {
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
directButtons.forEach((button) => handled.add(button));
|
|
2805
|
+
groups.push(group);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
return groups.slice(0, 6);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function normalizeDateValue(value: string | null | undefined): string | undefined {
|
|
2812
|
+
const normalized = cleanControlText(value);
|
|
2813
|
+
if (!normalized) {
|
|
2814
|
+
return undefined;
|
|
2815
|
+
}
|
|
2816
|
+
return Number.isFinite(Date.parse(normalized)) || /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : undefined;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function collectDateControls(root: ParentNode): InspectPageDateControl[] {
|
|
2820
|
+
const controls: InspectPageDateControl[] = [];
|
|
2821
|
+
|
|
2822
|
+
for (const input of Array.from(root.querySelectorAll<HTMLInputElement>('input[type="date"], input[type="datetime-local"], input[type="month"], input[type="week"]'))) {
|
|
2823
|
+
controls.push({
|
|
2824
|
+
controlType: 'input',
|
|
2825
|
+
label: extractElementLabel(input),
|
|
2826
|
+
selectorHint: selectorHintForElement(input),
|
|
2827
|
+
value: normalizeDateValue(input.value),
|
|
2828
|
+
min: normalizeDateValue(input.min),
|
|
2829
|
+
max: normalizeDateValue(input.max),
|
|
2830
|
+
dataMaxDate: normalizeDateValue(input.getAttribute('data-max-date'))
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
for (const select of Array.from(root.querySelectorAll('select'))) {
|
|
2835
|
+
const optionValues = Array.from(select.querySelectorAll('option'))
|
|
2836
|
+
.map((option) => normalizeDateValue(option.value) ?? normalizeDateValue(option.textContent))
|
|
2837
|
+
.filter((value): value is string => typeof value === 'string');
|
|
2838
|
+
const dataMaxDate = normalizeDateValue(select.getAttribute('data-max-date'));
|
|
2839
|
+
if (optionValues.length === 0 && !dataMaxDate) {
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
controls.push({
|
|
2843
|
+
controlType: 'select',
|
|
2844
|
+
label: extractElementLabel(select),
|
|
2845
|
+
selectorHint: selectorHintForElement(select),
|
|
2846
|
+
value: normalizeDateValue((select as HTMLSelectElement).value),
|
|
2847
|
+
dataMaxDate,
|
|
2848
|
+
options: [...new Set(optionValues)].slice(0, 8)
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
for (const element of Array.from(root.querySelectorAll<HTMLElement>('[data-max-date], [data-date]'))) {
|
|
2853
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement) {
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
const dataMaxDate = normalizeDateValue(element.getAttribute('data-max-date'));
|
|
2857
|
+
const value = normalizeDateValue(element.getAttribute('data-date'));
|
|
2858
|
+
if (!dataMaxDate && !value) {
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
controls.push({
|
|
2862
|
+
controlType: 'dataset',
|
|
2863
|
+
label: extractElementLabel(element),
|
|
2864
|
+
selectorHint: selectorHintForElement(element),
|
|
2865
|
+
value,
|
|
2866
|
+
dataMaxDate
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
return controls.slice(0, 10);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2638
2873
|
async function collectInspectionState(params: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
2639
2874
|
const rootResult = resolveRootForLocator();
|
|
2640
2875
|
if (!rootResult.ok) {
|
|
@@ -2656,6 +2891,8 @@ async function collectInspectionState(params: Record<string, unknown> = {}): Pro
|
|
|
2656
2891
|
const previewGlobals = await globalsPreview();
|
|
2657
2892
|
const suspiciousGlobals = [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50);
|
|
2658
2893
|
const inlineJsonSources = collectInlineJsonSources(root);
|
|
2894
|
+
const modeGroups = collectModeGroups(root);
|
|
2895
|
+
const dateControls = collectDateControls(root);
|
|
2659
2896
|
return {
|
|
2660
2897
|
url: metadata.url,
|
|
2661
2898
|
title: metadata.title,
|
|
@@ -2675,6 +2912,8 @@ async function collectInspectionState(params: Record<string, unknown> = {}): Pro
|
|
|
2675
2912
|
cookies: cookieMetadata(),
|
|
2676
2913
|
frames: collectFrames(),
|
|
2677
2914
|
inlineJsonSources,
|
|
2915
|
+
modeGroups,
|
|
2916
|
+
dateControls,
|
|
2678
2917
|
tables: describeTables(),
|
|
2679
2918
|
timers: {
|
|
2680
2919
|
timeouts: 0,
|
|
@@ -2,8 +2,12 @@ import type {
|
|
|
2
2
|
DynamicDataSchemaHint,
|
|
3
3
|
FreshnessTimestampCategory,
|
|
4
4
|
InspectPageDataCandidateProbe,
|
|
5
|
+
InspectPageCurrentMode,
|
|
6
|
+
InspectPageDateControl,
|
|
5
7
|
InspectPageDataRecommendation,
|
|
6
8
|
InspectPageDataResult,
|
|
9
|
+
InspectPageModeGroup,
|
|
10
|
+
InspectPagePrimaryEndpoint,
|
|
7
11
|
InspectPageDataSource,
|
|
8
12
|
InspectPageDataSourceMapping,
|
|
9
13
|
NetworkEntry,
|
|
@@ -59,6 +63,7 @@ export interface SourceMappingInput {
|
|
|
59
63
|
windowSources: InspectPageDataCandidateProbe[];
|
|
60
64
|
inlineJsonSources: InlineJsonInspectionSource[];
|
|
61
65
|
recentNetwork: NetworkEntry[];
|
|
66
|
+
pageUrl?: string;
|
|
62
67
|
now?: number;
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -67,6 +72,7 @@ export interface SourceMappingReport {
|
|
|
67
72
|
sourceMappings: InspectPageDataSourceMapping[];
|
|
68
73
|
recommendedNextActions: InspectPageDataRecommendation[];
|
|
69
74
|
sourceAnalyses: DynamicSourceAnalysis[];
|
|
75
|
+
primaryEndpoint: InspectPagePrimaryEndpoint | null;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
export interface ReplaySchemaMatch {
|
|
@@ -530,9 +536,9 @@ function scoreSourceMapping(table: TableAnalysis, source: DynamicSourceAnalysis,
|
|
|
530
536
|
}
|
|
531
537
|
|
|
532
538
|
const explicitReferenceHit =
|
|
533
|
-
table.table.
|
|
539
|
+
table.table.label.toLowerCase().includes(source.source.label.toLowerCase()) ||
|
|
534
540
|
(table.table.selector ?? '').toLowerCase().includes(source.source.label.toLowerCase()) ||
|
|
535
|
-
source.source.label.toLowerCase().includes(table.table.
|
|
541
|
+
source.source.label.toLowerCase().includes(table.table.label.toLowerCase());
|
|
536
542
|
if (explicitReferenceHit) {
|
|
537
543
|
basis.push({
|
|
538
544
|
type: 'explicitReference',
|
|
@@ -609,8 +615,8 @@ function buildRecommendedNextActions(
|
|
|
609
615
|
if (source.source.type === 'networkResponse') {
|
|
610
616
|
const requestId = source.source.sourceId.replace(/^networkResponse:/, '');
|
|
611
617
|
pushRecommendation({
|
|
612
|
-
title: `
|
|
613
|
-
command: `bak network
|
|
618
|
+
title: `Clone ${requestId} into a reusable fetch template`,
|
|
619
|
+
command: `bak network clone ${requestId}`,
|
|
614
620
|
note: `Recent response mapped to ${mapping.tableId} with ${mapping.confidence} confidence.`
|
|
615
621
|
});
|
|
616
622
|
continue;
|
|
@@ -632,6 +638,122 @@ function buildRecommendedNextActions(
|
|
|
632
638
|
return recommendations.slice(0, 6);
|
|
633
639
|
}
|
|
634
640
|
|
|
641
|
+
function confidenceRank(confidence: InspectPageDataSourceMapping['confidence']): number {
|
|
642
|
+
switch (confidence) {
|
|
643
|
+
case 'high':
|
|
644
|
+
return 0;
|
|
645
|
+
case 'medium':
|
|
646
|
+
return 1;
|
|
647
|
+
case 'low':
|
|
648
|
+
return 2;
|
|
649
|
+
default:
|
|
650
|
+
return 3;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isSameOrigin(pageUrl: string | undefined, requestUrl: string): boolean {
|
|
655
|
+
if (!pageUrl) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const page = new URL(pageUrl);
|
|
660
|
+
const request = new URL(requestUrl, page);
|
|
661
|
+
return page.origin === request.origin;
|
|
662
|
+
} catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function selectPrimaryEndpoint(
|
|
668
|
+
recentNetwork: NetworkEntry[],
|
|
669
|
+
mappings: InspectPageDataSourceMapping[],
|
|
670
|
+
pageUrl?: string
|
|
671
|
+
): InspectPagePrimaryEndpoint | null {
|
|
672
|
+
const mapped = mappings
|
|
673
|
+
.filter((mapping) => mapping.sourceId.startsWith('networkResponse:'))
|
|
674
|
+
.map((mapping) => ({
|
|
675
|
+
mapping,
|
|
676
|
+
entry: recentNetwork.find((entry) => entry.id === mapping.sourceId.replace(/^networkResponse:/, ''))
|
|
677
|
+
}))
|
|
678
|
+
.filter((candidate): candidate is { mapping: InspectPageDataSourceMapping; entry: NetworkEntry } => candidate.entry !== undefined)
|
|
679
|
+
.sort((left, right) => {
|
|
680
|
+
return (
|
|
681
|
+
confidenceRank(left.mapping.confidence) - confidenceRank(right.mapping.confidence) ||
|
|
682
|
+
right.entry.ts - left.entry.ts ||
|
|
683
|
+
left.entry.id.localeCompare(right.entry.id)
|
|
684
|
+
);
|
|
685
|
+
})[0];
|
|
686
|
+
|
|
687
|
+
if (mapped) {
|
|
688
|
+
return {
|
|
689
|
+
requestId: mapped.entry.id,
|
|
690
|
+
url: mapped.entry.url,
|
|
691
|
+
method: mapped.entry.method,
|
|
692
|
+
status: mapped.entry.status,
|
|
693
|
+
kind: mapped.entry.kind,
|
|
694
|
+
resourceType: mapped.entry.resourceType,
|
|
695
|
+
contentType: mapped.entry.contentType,
|
|
696
|
+
sameOrigin: isSameOrigin(pageUrl, mapped.entry.url),
|
|
697
|
+
matchedTableId: mapped.mapping.tableId,
|
|
698
|
+
matchedSourceId: mapped.mapping.sourceId,
|
|
699
|
+
reason: `Mapped to ${mapped.mapping.tableId} with ${mapped.mapping.confidence} confidence`
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const fallback = recentNetwork
|
|
704
|
+
.filter((entry) => (entry.kind === 'fetch' || entry.kind === 'xhr') && entry.status >= 200 && entry.status < 400)
|
|
705
|
+
.sort((left, right) => right.ts - left.ts)[0];
|
|
706
|
+
if (!fallback) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
requestId: fallback.id,
|
|
711
|
+
url: fallback.url,
|
|
712
|
+
method: fallback.method,
|
|
713
|
+
status: fallback.status,
|
|
714
|
+
kind: fallback.kind,
|
|
715
|
+
resourceType: fallback.resourceType,
|
|
716
|
+
contentType: fallback.contentType,
|
|
717
|
+
sameOrigin: isSameOrigin(pageUrl, fallback.url),
|
|
718
|
+
reason: `Latest successful ${fallback.kind.toUpperCase()} request observed on the page`
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function summarizeAvailableModes(modeGroups: InspectPageModeGroup[]): string[] {
|
|
723
|
+
return [...new Set(modeGroups.flatMap((group) => group.options.map((option) => option.label)))];
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function selectCurrentMode(modeGroups: InspectPageModeGroup[]): InspectPageCurrentMode | null {
|
|
727
|
+
for (const group of modeGroups) {
|
|
728
|
+
const selected = group.options.find((option) => option.selected);
|
|
729
|
+
if (selected) {
|
|
730
|
+
return {
|
|
731
|
+
controlType: group.controlType,
|
|
732
|
+
label: selected.label,
|
|
733
|
+
value: selected.value,
|
|
734
|
+
groupLabel: group.label
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function deriveLatestArchiveDate(dateControls: InspectPageDateControl[]): string | null {
|
|
742
|
+
const candidates = dateControls.flatMap((control) => [
|
|
743
|
+
control.value,
|
|
744
|
+
control.min,
|
|
745
|
+
control.max,
|
|
746
|
+
control.dataMaxDate,
|
|
747
|
+
...(Array.isArray(control.options) ? control.options : [])
|
|
748
|
+
]);
|
|
749
|
+
const dated = candidates
|
|
750
|
+
.filter((value): value is string => typeof value === 'string')
|
|
751
|
+
.map((value) => ({ value, parsed: Date.parse(value) }))
|
|
752
|
+
.filter((item) => Number.isFinite(item.parsed))
|
|
753
|
+
.sort((left, right) => right.parsed - left.parsed);
|
|
754
|
+
return dated[0]?.value ?? null;
|
|
755
|
+
}
|
|
756
|
+
|
|
635
757
|
export function buildSourceMappingReport(input: SourceMappingInput): SourceMappingReport {
|
|
636
758
|
const now = typeof input.now === 'number' ? input.now : Date.now();
|
|
637
759
|
const windowAnalyses = buildWindowSources(input.windowSources);
|
|
@@ -653,7 +775,8 @@ export function buildSourceMappingReport(input: SourceMappingInput): SourceMappi
|
|
|
653
775
|
dataSources: sourceAnalyses.map((analysis) => analysis.source),
|
|
654
776
|
sourceMappings,
|
|
655
777
|
recommendedNextActions: buildRecommendedNextActions(input.tables, sourceMappings, sourceAnalyses),
|
|
656
|
-
sourceAnalyses
|
|
778
|
+
sourceAnalyses,
|
|
779
|
+
primaryEndpoint: selectPrimaryEndpoint(input.recentNetwork, sourceMappings, input.pageUrl)
|
|
657
780
|
};
|
|
658
781
|
}
|
|
659
782
|
|
|
@@ -752,6 +875,7 @@ export function selectReplaySchemaMatch(
|
|
|
752
875
|
}
|
|
753
876
|
|
|
754
877
|
export function buildInspectPageDataResult(input: {
|
|
878
|
+
pageUrl?: string;
|
|
755
879
|
suspiciousGlobals: string[];
|
|
756
880
|
tables: TableHandle[];
|
|
757
881
|
visibleTimestamps: string[];
|
|
@@ -760,19 +884,29 @@ export function buildInspectPageDataResult(input: {
|
|
|
760
884
|
recentNetwork: NetworkEntry[];
|
|
761
885
|
tableAnalyses: TableAnalysis[];
|
|
762
886
|
inlineJsonSources: InlineJsonInspectionSource[];
|
|
887
|
+
modeGroups: InspectPageModeGroup[];
|
|
888
|
+
dateControls: InspectPageDateControl[];
|
|
763
889
|
now?: number;
|
|
764
|
-
}): Pick<
|
|
890
|
+
}): Pick<
|
|
891
|
+
InspectPageDataResult,
|
|
892
|
+
'dataSources' | 'sourceMappings' | 'recommendedNextActions' | 'availableModes' | 'currentMode' | 'latestArchiveDate' | 'primaryEndpoint'
|
|
893
|
+
> {
|
|
765
894
|
const report = buildSourceMappingReport({
|
|
766
895
|
tables: input.tableAnalyses,
|
|
767
896
|
windowSources: input.pageDataCandidates,
|
|
768
897
|
inlineJsonSources: input.inlineJsonSources,
|
|
769
898
|
recentNetwork: input.recentNetwork,
|
|
899
|
+
pageUrl: input.pageUrl,
|
|
770
900
|
now: input.now
|
|
771
901
|
});
|
|
772
902
|
return {
|
|
773
903
|
dataSources: report.dataSources,
|
|
774
904
|
sourceMappings: report.sourceMappings,
|
|
775
|
-
recommendedNextActions: report.recommendedNextActions
|
|
905
|
+
recommendedNextActions: report.recommendedNextActions,
|
|
906
|
+
availableModes: summarizeAvailableModes(input.modeGroups),
|
|
907
|
+
currentMode: selectCurrentMode(input.modeGroups),
|
|
908
|
+
latestArchiveDate: deriveLatestArchiveDate(input.dateControls),
|
|
909
|
+
primaryEndpoint: report.primaryEndpoint
|
|
776
910
|
};
|
|
777
911
|
}
|
|
778
912
|
|