@wordbricks/playwright-mcp 0.1.20 → 0.1.23
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/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/package.json +34 -57
- package/LICENSE +0 -202
- package/lib/browserContextFactory.js +0 -326
- package/lib/browserServerBackend.js +0 -84
- package/lib/config.js +0 -286
- package/lib/context.js +0 -309
- package/lib/extension/cdpRelay.js +0 -346
- package/lib/extension/extensionContextFactory.js +0 -56
- package/lib/frameworkPatterns.js +0 -35
- package/lib/hooks/antiBotDetectionHook.js +0 -171
- package/lib/hooks/core.js +0 -144
- package/lib/hooks/eventConsumer.js +0 -52
- package/lib/hooks/events.js +0 -42
- package/lib/hooks/formatToolCallEvent.js +0 -16
- package/lib/hooks/frameworkStateHook.js +0 -182
- package/lib/hooks/grouping.js +0 -72
- package/lib/hooks/jsonLdDetectionHook.js +0 -175
- package/lib/hooks/networkFilters.js +0 -82
- package/lib/hooks/networkSetup.js +0 -59
- package/lib/hooks/networkTrackingHook.js +0 -67
- package/lib/hooks/pageHeightHook.js +0 -75
- package/lib/hooks/registry.js +0 -42
- package/lib/hooks/requireTabHook.js +0 -26
- package/lib/hooks/schema.js +0 -89
- package/lib/hooks/waitHook.js +0 -33
- package/lib/index.js +0 -39
- package/lib/mcp/inProcessTransport.js +0 -72
- package/lib/mcp/proxyBackend.js +0 -115
- package/lib/mcp/server.js +0 -86
- package/lib/mcp/tool.js +0 -38
- package/lib/mcp/transport.js +0 -181
- package/lib/playwrightTransformer.js +0 -497
- package/lib/program.js +0 -110
- package/lib/response.js +0 -186
- package/lib/sessionLog.js +0 -121
- package/lib/tab.js +0 -249
- package/lib/tools/common.js +0 -55
- package/lib/tools/console.js +0 -33
- package/lib/tools/dialogs.js +0 -47
- package/lib/tools/evaluate.js +0 -53
- package/lib/tools/extractFrameworkState.js +0 -214
- package/lib/tools/files.js +0 -45
- package/lib/tools/form.js +0 -57
- package/lib/tools/getSnapshot.js +0 -37
- package/lib/tools/getVisibleHtml.js +0 -52
- package/lib/tools/install.js +0 -51
- package/lib/tools/keyboard.js +0 -78
- package/lib/tools/mouse.js +0 -99
- package/lib/tools/navigate.js +0 -70
- package/lib/tools/network.js +0 -123
- package/lib/tools/networkDetail.js +0 -229
- package/lib/tools/networkSearch/bodySearch.js +0 -147
- package/lib/tools/networkSearch/grouping.js +0 -28
- package/lib/tools/networkSearch/helpers.js +0 -32
- package/lib/tools/networkSearch/searchHtml.js +0 -67
- package/lib/tools/networkSearch/types.js +0 -1
- package/lib/tools/networkSearch/urlSearch.js +0 -82
- package/lib/tools/networkSearch.js +0 -268
- package/lib/tools/pdf.js +0 -40
- package/lib/tools/repl.js +0 -402
- package/lib/tools/screenshot.js +0 -79
- package/lib/tools/scroll.js +0 -126
- package/lib/tools/snapshot.js +0 -144
- package/lib/tools/tabs.js +0 -59
- package/lib/tools/tool.js +0 -33
- package/lib/tools/utils.js +0 -74
- package/lib/tools/wait.js +0 -55
- package/lib/tools.js +0 -67
- package/lib/utils/adBlockFilter.js +0 -87
- package/lib/utils/codegen.js +0 -51
- package/lib/utils/extensionPath.js +0 -10
- package/lib/utils/fileUtils.js +0 -36
- package/lib/utils/graphql.js +0 -258
- package/lib/utils/guid.js +0 -22
- package/lib/utils/httpServer.js +0 -39
- package/lib/utils/log.js +0 -21
- package/lib/utils/manualPromise.js +0 -111
- package/lib/utils/networkFormat.js +0 -12
- package/lib/utils/package.js +0 -20
- package/lib/utils/result.js +0 -2
- package/lib/utils/sanitizeHtml.js +0 -98
- package/lib/utils/truncate.js +0 -103
- package/lib/utils/withTimeout.js +0 -7
- package/src/index.ts +0 -50
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { Ok } from '../utils/result.js';
|
|
2
|
-
import { hookNameSchema } from './schema.js';
|
|
3
|
-
import { trackEvent } from './events.js';
|
|
4
|
-
import { FRAMEWORK_STATE_PATTERNS, MAX_DISPLAY_ITEMS } from '../frameworkPatterns.js';
|
|
5
|
-
const pageFrameworkStates = new WeakMap();
|
|
6
|
-
const seenFrameworkKeysByContext = new WeakMap();
|
|
7
|
-
const getSeenFrameworkKeys = (context) => {
|
|
8
|
-
let set = seenFrameworkKeysByContext.get(context);
|
|
9
|
-
if (!set) {
|
|
10
|
-
set = new Set();
|
|
11
|
-
seenFrameworkKeysByContext.set(context, set);
|
|
12
|
-
}
|
|
13
|
-
return set;
|
|
14
|
-
};
|
|
15
|
-
export const frameworkStatePreHook = {
|
|
16
|
-
name: hookNameSchema.enum['framework-state-pre'],
|
|
17
|
-
handler: async (context) => {
|
|
18
|
-
const frameworkState = await detectFrameworkState(context);
|
|
19
|
-
if (frameworkState) {
|
|
20
|
-
// Store the initial state
|
|
21
|
-
if (context.tab?.page)
|
|
22
|
-
pageFrameworkStates.set(context.tab.page, frameworkState);
|
|
23
|
-
// Track event for newly detected framework state
|
|
24
|
-
const newKeys = Object.keys(frameworkState).filter(key => !getSeenFrameworkKeys(context.context).has(key));
|
|
25
|
-
if (newKeys.length > 0) {
|
|
26
|
-
trackEvent(context.context, {
|
|
27
|
-
type: 'framework-state',
|
|
28
|
-
data: {
|
|
29
|
-
state: frameworkState,
|
|
30
|
-
action: 'detected',
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
// Mark keys as seen
|
|
34
|
-
newKeys.forEach(key => getSeenFrameworkKeys(context.context).add(key));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return Ok(undefined);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
export const frameworkStatePostHook = {
|
|
41
|
-
name: hookNameSchema.enum['framework-state-post'],
|
|
42
|
-
handler: async (context) => {
|
|
43
|
-
const newFrameworkState = await detectFrameworkState(context);
|
|
44
|
-
const initialState = context.tab?.page ? pageFrameworkStates.get(context.tab.page) : undefined;
|
|
45
|
-
if (newFrameworkState) {
|
|
46
|
-
const changes = [];
|
|
47
|
-
if (initialState) {
|
|
48
|
-
// Compare states
|
|
49
|
-
const allKeys = new Set([
|
|
50
|
-
...Object.keys(initialState),
|
|
51
|
-
...Object.keys(newFrameworkState),
|
|
52
|
-
]);
|
|
53
|
-
for (const key of allKeys) {
|
|
54
|
-
const initVal = initialState[key];
|
|
55
|
-
const currVal = newFrameworkState[key];
|
|
56
|
-
if (!initVal && currVal)
|
|
57
|
-
changes.push(`+ ${key}: ${formatValue(currVal)}`);
|
|
58
|
-
else if (initVal && !currVal)
|
|
59
|
-
changes.push(`- ${key}`);
|
|
60
|
-
else if (initVal &&
|
|
61
|
-
currVal &&
|
|
62
|
-
JSON.stringify(initVal) !== JSON.stringify(currVal))
|
|
63
|
-
changes.push(`~ ${key}: changed`);
|
|
64
|
-
}
|
|
65
|
-
if (changes.length > 0) {
|
|
66
|
-
trackEvent(context.context, {
|
|
67
|
-
type: 'framework-state',
|
|
68
|
-
data: {
|
|
69
|
-
state: newFrameworkState,
|
|
70
|
-
changes,
|
|
71
|
-
action: 'changed',
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
// No initial state, but we have state now
|
|
78
|
-
const newKeys = Object.keys(newFrameworkState).filter(key => !getSeenFrameworkKeys(context.context).has(key));
|
|
79
|
-
if (newKeys.length > 0) {
|
|
80
|
-
trackEvent(context.context, {
|
|
81
|
-
type: 'framework-state',
|
|
82
|
-
data: {
|
|
83
|
-
state: newFrameworkState,
|
|
84
|
-
action: 'detected',
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
newKeys.forEach(key => getSeenFrameworkKeys(context.context).add(key));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Update stored state
|
|
91
|
-
if (context.tab?.page)
|
|
92
|
-
pageFrameworkStates.set(context.tab.page, newFrameworkState);
|
|
93
|
-
}
|
|
94
|
-
return Ok(undefined);
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
async function detectFrameworkState(context) {
|
|
98
|
-
if (!context.tab?.page)
|
|
99
|
-
return null;
|
|
100
|
-
const result = await context.tab.page.evaluate((patterns) => {
|
|
101
|
-
const state = {};
|
|
102
|
-
const MAX_ITEMS = 5;
|
|
103
|
-
// Scan window object for these patterns
|
|
104
|
-
for (const pattern of patterns) {
|
|
105
|
-
if (pattern in window) {
|
|
106
|
-
try {
|
|
107
|
-
const value = window[pattern];
|
|
108
|
-
// Only capture if it's a non-empty object or has meaningful content
|
|
109
|
-
if (value &&
|
|
110
|
-
(typeof value === 'object' || typeof value === 'string')) {
|
|
111
|
-
state[pattern] =
|
|
112
|
-
typeof value === 'object'
|
|
113
|
-
? {
|
|
114
|
-
type: 'object',
|
|
115
|
-
keys: Object.keys(value).slice(0, MAX_ITEMS * 2),
|
|
116
|
-
}
|
|
117
|
-
: {
|
|
118
|
-
type: typeof value,
|
|
119
|
-
preview: String(value).slice(0, 200),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch (e) {
|
|
124
|
-
// Skip inaccessible properties
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// Also check for React Fiber internals
|
|
129
|
-
const reactRootSelectors = [
|
|
130
|
-
'#__next',
|
|
131
|
-
'#root',
|
|
132
|
-
'#app',
|
|
133
|
-
'[data-reactroot]',
|
|
134
|
-
];
|
|
135
|
-
for (const selector of reactRootSelectors) {
|
|
136
|
-
const element = document.querySelector(selector);
|
|
137
|
-
if (element) {
|
|
138
|
-
const fiberKey = Object.keys(element).find(key => key.startsWith('__reactInternalInstance') ||
|
|
139
|
-
key.startsWith('__reactFiber') ||
|
|
140
|
-
key.startsWith('_reactRootContainer'));
|
|
141
|
-
if (fiberKey) {
|
|
142
|
-
state['React Fiber Root'] = { selector, fiberKey };
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return Object.keys(state).length > 0 ? state : null;
|
|
148
|
-
}, FRAMEWORK_STATE_PATTERNS);
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
|
-
function formatValue(value) {
|
|
152
|
-
if (typeof value === 'object' && value !== null && 'type' in value) {
|
|
153
|
-
if (value.type === 'object' &&
|
|
154
|
-
'keys' in value &&
|
|
155
|
-
Array.isArray(value.keys))
|
|
156
|
-
return `{${value.keys.join(', ')}${value.keys.length >= MAX_DISPLAY_ITEMS * 2 ? ', ...' : ''}}`;
|
|
157
|
-
else if ('preview' in value && typeof value.preview === 'string')
|
|
158
|
-
return `"${value.preview}${value.preview.length >= 200 ? '...' : ''}"`;
|
|
159
|
-
}
|
|
160
|
-
return JSON.stringify(value);
|
|
161
|
-
}
|
|
162
|
-
export const formatFrameworkStateEvent = (event) => {
|
|
163
|
-
const { state, changes, action } = event.data;
|
|
164
|
-
const messages = [];
|
|
165
|
-
if (action === 'detected') {
|
|
166
|
-
messages.push('Framework state detected:');
|
|
167
|
-
const keys = Object.keys(state);
|
|
168
|
-
for (const key of keys) {
|
|
169
|
-
const value = state[key];
|
|
170
|
-
messages.push(` ${key}: ${formatValue(value)}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
else if (action === 'changed' && changes) {
|
|
174
|
-
messages.push('Framework state changed:');
|
|
175
|
-
messages.push(...changes.map((change) => ` ${change}`));
|
|
176
|
-
}
|
|
177
|
-
return messages.join('\n');
|
|
178
|
-
};
|
|
179
|
-
export const frameworkStateHooks = {
|
|
180
|
-
pre: frameworkStatePreHook,
|
|
181
|
-
post: frameworkStatePostHook,
|
|
182
|
-
};
|
package/lib/hooks/grouping.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
const rules = new Map();
|
|
2
|
-
// Helper to define a rule with typed callbacks without using type assertions in callers
|
|
3
|
-
export const defineGroupingRule = (spec) => {
|
|
4
|
-
const { match } = spec;
|
|
5
|
-
return {
|
|
6
|
-
match,
|
|
7
|
-
keyOf: (e) => {
|
|
8
|
-
// Planner guarantees keyOf is only called when match(e) is true
|
|
9
|
-
if (!match(e))
|
|
10
|
-
return '';
|
|
11
|
-
return spec.keyOf(e);
|
|
12
|
-
},
|
|
13
|
-
summaryOf: (first, run) => {
|
|
14
|
-
// Build a typed run defensively using the provided type guard
|
|
15
|
-
const typedRun = [];
|
|
16
|
-
for (const ev of run) {
|
|
17
|
-
if (match(ev))
|
|
18
|
-
typedRun.push(ev);
|
|
19
|
-
}
|
|
20
|
-
const typedFirst = match(first) ? first : typedRun[0];
|
|
21
|
-
if (!typedFirst)
|
|
22
|
-
return '';
|
|
23
|
-
return spec.summaryOf(typedFirst, typedRun);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
export const registerGroupingRule = (type, rule) => {
|
|
28
|
-
rules.set(type, rule);
|
|
29
|
-
};
|
|
30
|
-
const getGroupingRule = (type) => {
|
|
31
|
-
return rules.get(type);
|
|
32
|
-
};
|
|
33
|
-
export const planGroupedMessages = (events) => {
|
|
34
|
-
const replacementById = new Map();
|
|
35
|
-
const skipIds = new Set();
|
|
36
|
-
let currentType;
|
|
37
|
-
let currentKey;
|
|
38
|
-
let run = [];
|
|
39
|
-
const flush = () => {
|
|
40
|
-
if (currentType && run.length > 1) {
|
|
41
|
-
const rule = getGroupingRule(currentType);
|
|
42
|
-
if (rule) {
|
|
43
|
-
const first = run[0];
|
|
44
|
-
const summary = rule.summaryOf(first, run);
|
|
45
|
-
replacementById.set(first.id, summary);
|
|
46
|
-
for (let i = 1; i < run.length; i++)
|
|
47
|
-
skipIds.add(run[i].id);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
currentType = undefined;
|
|
51
|
-
currentKey = undefined;
|
|
52
|
-
run = [];
|
|
53
|
-
};
|
|
54
|
-
for (const ev of events) {
|
|
55
|
-
const rule = getGroupingRule(ev.type);
|
|
56
|
-
if (!rule || !rule.match(ev)) {
|
|
57
|
-
flush();
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const key = rule.keyOf(ev);
|
|
61
|
-
if (currentType === ev.type && currentKey === key) {
|
|
62
|
-
run.push(ev);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
flush();
|
|
66
|
-
currentType = ev.type;
|
|
67
|
-
currentKey = key;
|
|
68
|
-
run = [ev];
|
|
69
|
-
}
|
|
70
|
-
flush();
|
|
71
|
-
return { replacementById, skipIds };
|
|
72
|
-
};
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { Ok } from '../utils/result.js';
|
|
2
|
-
import { hookNameSchema } from './schema.js';
|
|
3
|
-
import { trackEvent } from './events.js';
|
|
4
|
-
const MAX_DISPLAY_ITEMS = 5;
|
|
5
|
-
const pageJsonLdStates = new WeakMap();
|
|
6
|
-
const seenJsonLdTypesByContext = new WeakMap();
|
|
7
|
-
const getSeenJsonLdTypes = (context) => {
|
|
8
|
-
let set = seenJsonLdTypesByContext.get(context);
|
|
9
|
-
if (!set) {
|
|
10
|
-
set = new Set();
|
|
11
|
-
seenJsonLdTypesByContext.set(context, set);
|
|
12
|
-
}
|
|
13
|
-
return set;
|
|
14
|
-
};
|
|
15
|
-
export const jsonLdDetectionPreHook = {
|
|
16
|
-
name: hookNameSchema.enum['json-ld-detection-pre'],
|
|
17
|
-
handler: async (context) => {
|
|
18
|
-
const jsonLdState = await detectJsonLdState(context);
|
|
19
|
-
if (jsonLdState) {
|
|
20
|
-
// Store the initial state
|
|
21
|
-
if (context.tab?.page)
|
|
22
|
-
pageJsonLdStates.set(context.tab.page, jsonLdState);
|
|
23
|
-
// Track event for newly detected JSON-LD types
|
|
24
|
-
const newTypes = Object.keys(jsonLdState).filter(type => !getSeenJsonLdTypes(context.context).has(type));
|
|
25
|
-
if (newTypes.length > 0) {
|
|
26
|
-
trackEvent(context.context, {
|
|
27
|
-
type: 'json-ld',
|
|
28
|
-
data: {
|
|
29
|
-
state: jsonLdState,
|
|
30
|
-
action: 'detected',
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
// Mark types as seen
|
|
34
|
-
newTypes.forEach(type => getSeenJsonLdTypes(context.context).add(type));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return Ok(undefined);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
export const jsonLdDetectionPostHook = {
|
|
41
|
-
name: hookNameSchema.enum['json-ld-detection-post'],
|
|
42
|
-
handler: async (context) => {
|
|
43
|
-
const newJsonLdState = await detectJsonLdState(context);
|
|
44
|
-
const initialState = context.tab?.page ? pageJsonLdStates.get(context.tab.page) : undefined;
|
|
45
|
-
if (newJsonLdState || initialState) {
|
|
46
|
-
const changes = [];
|
|
47
|
-
if (initialState && newJsonLdState) {
|
|
48
|
-
// Compare states
|
|
49
|
-
const allTypes = new Set([
|
|
50
|
-
...Object.keys(initialState),
|
|
51
|
-
...Object.keys(newJsonLdState),
|
|
52
|
-
]);
|
|
53
|
-
for (const type of allTypes) {
|
|
54
|
-
const initInfo = initialState[type];
|
|
55
|
-
const currInfo = newJsonLdState[type];
|
|
56
|
-
if (!initInfo && currInfo)
|
|
57
|
-
changes.push(`+ ${type}${currInfo.count > 1 ? ` (${currInfo.count} instances)` : ''}`);
|
|
58
|
-
else if (initInfo && !currInfo)
|
|
59
|
-
changes.push(`- ${type}${initInfo.count > 1 ? ` (${initInfo.count} instances)` : ''}`);
|
|
60
|
-
else if (initInfo &&
|
|
61
|
-
currInfo &&
|
|
62
|
-
initInfo.count !== currInfo.count)
|
|
63
|
-
changes.push(`~ ${type}: ${initInfo.count} → ${currInfo.count} instances`);
|
|
64
|
-
}
|
|
65
|
-
if (changes.length > 0) {
|
|
66
|
-
trackEvent(context.context, {
|
|
67
|
-
type: 'json-ld',
|
|
68
|
-
data: {
|
|
69
|
-
state: newJsonLdState,
|
|
70
|
-
changes,
|
|
71
|
-
action: 'changed',
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else if (newJsonLdState && !initialState) {
|
|
77
|
-
// No initial state, but we have state now
|
|
78
|
-
const newTypes = Object.keys(newJsonLdState).filter(type => !getSeenJsonLdTypes(context.context).has(type));
|
|
79
|
-
if (newTypes.length > 0) {
|
|
80
|
-
trackEvent(context.context, {
|
|
81
|
-
type: 'json-ld',
|
|
82
|
-
data: {
|
|
83
|
-
state: newJsonLdState,
|
|
84
|
-
action: 'detected',
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
newTypes.forEach(type => getSeenJsonLdTypes(context.context).add(type));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Update stored state
|
|
91
|
-
if (context.tab?.page && newJsonLdState)
|
|
92
|
-
pageJsonLdStates.set(context.tab.page, newJsonLdState);
|
|
93
|
-
}
|
|
94
|
-
return Ok(undefined);
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
async function detectJsonLdState(context) {
|
|
98
|
-
if (!context.tab?.page)
|
|
99
|
-
return null;
|
|
100
|
-
const result = await context.tab.page.evaluate(() => {
|
|
101
|
-
const state = {};
|
|
102
|
-
// Find all JSON-LD scripts
|
|
103
|
-
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
104
|
-
scripts.forEach((script, index) => {
|
|
105
|
-
try {
|
|
106
|
-
// Parse JSON
|
|
107
|
-
const data = JSON.parse(script.textContent || '{}');
|
|
108
|
-
// Extract @type - handle both single and array types
|
|
109
|
-
let types = [];
|
|
110
|
-
if (data['@type']) {
|
|
111
|
-
types = Array.isArray(data['@type']) ? data['@type'] : [data['@type']];
|
|
112
|
-
}
|
|
113
|
-
else if (data['@graph'] && Array.isArray(data['@graph'])) {
|
|
114
|
-
// Handle @graph structures
|
|
115
|
-
data['@graph'].forEach((item) => {
|
|
116
|
-
if (item['@type']) {
|
|
117
|
-
const itemTypes = Array.isArray(item['@type']) ? item['@type'] : [item['@type']];
|
|
118
|
-
types.push(...itemTypes);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
// Count occurrences of each type
|
|
123
|
-
types.forEach(type => {
|
|
124
|
-
if (!state[type])
|
|
125
|
-
state[type] = { count: 0, indices: [] };
|
|
126
|
-
state[type].count++;
|
|
127
|
-
state[type].indices.push(index);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
state['InvalidJSON-LD'] = state['InvalidJSON-LD'] || { count: 0, indices: [] };
|
|
132
|
-
state['InvalidJSON-LD'].count++;
|
|
133
|
-
state['InvalidJSON-LD'].indices.push(index);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
return Object.keys(state).length > 0 ? state : null;
|
|
137
|
-
});
|
|
138
|
-
return result;
|
|
139
|
-
}
|
|
140
|
-
function buildStateMessages(state, types) {
|
|
141
|
-
const msgs = [];
|
|
142
|
-
const targetTypes = types ?? Object.keys(state);
|
|
143
|
-
// Sort by count (descending) and take top MAX_DISPLAY_ITEMS
|
|
144
|
-
const sortedTypes = targetTypes
|
|
145
|
-
.sort((a, b) => (state[b].count || 0) - (state[a].count || 0))
|
|
146
|
-
.slice(0, MAX_DISPLAY_ITEMS);
|
|
147
|
-
for (const type of sortedTypes) {
|
|
148
|
-
const info = state[type];
|
|
149
|
-
if (info.count === 1)
|
|
150
|
-
msgs.push(` ${type}`);
|
|
151
|
-
else
|
|
152
|
-
msgs.push(` ${type} (${info.count} instances)`);
|
|
153
|
-
}
|
|
154
|
-
// Add indicator if there are more types
|
|
155
|
-
if (targetTypes.length > MAX_DISPLAY_ITEMS)
|
|
156
|
-
msgs.push(` ... and ${targetTypes.length - MAX_DISPLAY_ITEMS} more type(s)`);
|
|
157
|
-
return msgs;
|
|
158
|
-
}
|
|
159
|
-
export const formatJsonLdEvent = (event) => {
|
|
160
|
-
const { state, changes, action } = event.data;
|
|
161
|
-
const messages = [];
|
|
162
|
-
if (action === 'detected') {
|
|
163
|
-
messages.push('New JSON-LD types detected:');
|
|
164
|
-
messages.push(...buildStateMessages(state));
|
|
165
|
-
}
|
|
166
|
-
else if (action === 'changed' && changes) {
|
|
167
|
-
messages.push('JSON-LD changes after action:');
|
|
168
|
-
messages.push(...changes.map((change) => ` ${change}`));
|
|
169
|
-
}
|
|
170
|
-
return messages.join('\n');
|
|
171
|
-
};
|
|
172
|
-
export const jsonLdDetectionHooks = {
|
|
173
|
-
pre: jsonLdDetectionPreHook,
|
|
174
|
-
post: jsonLdDetectionPostHook,
|
|
175
|
-
};
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
const MEANINGFUL_RESOURCE_TYPES = ['document', 'xhr', 'fetch'];
|
|
2
|
-
const ALLOWED_METHODS = ['GET', 'POST'];
|
|
3
|
-
const EXCLUDED_EXTENSIONS = [
|
|
4
|
-
'.svg',
|
|
5
|
-
'.css',
|
|
6
|
-
'.map', // JS files and source maps
|
|
7
|
-
];
|
|
8
|
-
const hasExcludedExtension = (url) => {
|
|
9
|
-
return EXCLUDED_EXTENSIONS.some(ext => {
|
|
10
|
-
const extRegex = new RegExp(`${ext.replace('.', '\\.')}(\\?|#|$)`, 'i');
|
|
11
|
-
return extRegex.test(url);
|
|
12
|
-
});
|
|
13
|
-
};
|
|
14
|
-
export const isAntiBotUrl = (url) => {
|
|
15
|
-
if (url.includes('challenges.cloudflare.com'))
|
|
16
|
-
return true;
|
|
17
|
-
if (url.includes('.awswaf.com'))
|
|
18
|
-
return true;
|
|
19
|
-
return false;
|
|
20
|
-
};
|
|
21
|
-
const isSuccessfulStatus = (status) => {
|
|
22
|
-
// Status 0 is for failed requests, which we want to capture
|
|
23
|
-
// 2xx status codes are successful
|
|
24
|
-
// Exclude 204 No Content
|
|
25
|
-
return status === 0 || (status >= 200 && status < 300 && status !== 204);
|
|
26
|
-
};
|
|
27
|
-
export const shouldCaptureRequest = (method, url, status, resourceType) => {
|
|
28
|
-
if (isAntiBotUrl(url))
|
|
29
|
-
return true;
|
|
30
|
-
return !hasExcludedExtension(url) &&
|
|
31
|
-
MEANINGFUL_RESOURCE_TYPES.includes(resourceType) &&
|
|
32
|
-
ALLOWED_METHODS.includes(method) &&
|
|
33
|
-
isSuccessfulStatus(status);
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Format URL with trimmed parameters
|
|
37
|
-
*/
|
|
38
|
-
export const formatUrlWithTrimmedParams = (url) => {
|
|
39
|
-
try {
|
|
40
|
-
const urlObj = new URL(url);
|
|
41
|
-
const params = urlObj.searchParams;
|
|
42
|
-
if (params.toString()) {
|
|
43
|
-
const trimmedParams = new URLSearchParams();
|
|
44
|
-
params.forEach((value, key) => {
|
|
45
|
-
if (value.length > 5)
|
|
46
|
-
trimmedParams.set(key, value.substring(0, 5) + '...');
|
|
47
|
-
else
|
|
48
|
-
trimmedParams.set(key, value);
|
|
49
|
-
});
|
|
50
|
-
return `${urlObj.origin}${urlObj.pathname}?${trimmedParams.toString()}`;
|
|
51
|
-
}
|
|
52
|
-
return url;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// If URL parsing fails, return as is
|
|
56
|
-
return url;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Normalize a pathname by removing trailing slashes (except root)
|
|
61
|
-
*/
|
|
62
|
-
export const normalizePathname = (pathname) => {
|
|
63
|
-
if (!pathname)
|
|
64
|
-
return '/';
|
|
65
|
-
if (pathname === '/')
|
|
66
|
-
return '/';
|
|
67
|
-
return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* Normalize URL for grouping by ignoring query/hash and trailing slash
|
|
71
|
-
*/
|
|
72
|
-
export const normalizeUrlForGrouping = (url) => {
|
|
73
|
-
try {
|
|
74
|
-
const u = new URL(url);
|
|
75
|
-
return `${u.origin}${normalizePathname(u.pathname)}`;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Fallback for non-standard/relative URLs: strip query/hash and trailing slash
|
|
79
|
-
const base = url.split(/[?#]/)[0] || '/';
|
|
80
|
-
return normalizePathname(base);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { trackEvent } from './events.js';
|
|
2
|
-
import { shouldCaptureRequest } from './networkFilters.js';
|
|
3
|
-
const eventIdToEntryMap = new WeakMap();
|
|
4
|
-
const getEventIdMap = (context) => {
|
|
5
|
-
let map = eventIdToEntryMap.get(context);
|
|
6
|
-
if (!map) {
|
|
7
|
-
map = new Map();
|
|
8
|
-
eventIdToEntryMap.set(context, map);
|
|
9
|
-
}
|
|
10
|
-
return map;
|
|
11
|
-
};
|
|
12
|
-
export const getNetworkEventEntry = (context, id) => getEventIdMap(context).get(id);
|
|
13
|
-
export const setupNetworkTracking = (context, page) => {
|
|
14
|
-
page.on('response', async (response) => {
|
|
15
|
-
const request = response.request();
|
|
16
|
-
const method = request.method();
|
|
17
|
-
const url = request.url();
|
|
18
|
-
const status = response.status();
|
|
19
|
-
const resourceType = request.resourceType();
|
|
20
|
-
// Apply filters before saving the event
|
|
21
|
-
if (shouldCaptureRequest(method, url, status, resourceType)) {
|
|
22
|
-
const setCookies = await response.headerValues('set-cookie').catch(() => []);
|
|
23
|
-
const cookieValues = setCookies.length ? setCookies : undefined;
|
|
24
|
-
const networkData = {
|
|
25
|
-
method,
|
|
26
|
-
url,
|
|
27
|
-
status,
|
|
28
|
-
resourceType,
|
|
29
|
-
postData: request.postData() || undefined,
|
|
30
|
-
setCookies: cookieValues,
|
|
31
|
-
};
|
|
32
|
-
const id = trackEvent(context, {
|
|
33
|
-
type: 'network-request',
|
|
34
|
-
data: networkData,
|
|
35
|
-
});
|
|
36
|
-
getEventIdMap(context).set(id, { request, response });
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
page.on('requestfailed', request => {
|
|
40
|
-
const method = request.method();
|
|
41
|
-
const url = request.url();
|
|
42
|
-
const status = 0; // Failed requests have status 0
|
|
43
|
-
const resourceType = request.resourceType();
|
|
44
|
-
if (shouldCaptureRequest(method, url, status, resourceType)) {
|
|
45
|
-
const networkData = {
|
|
46
|
-
method,
|
|
47
|
-
url,
|
|
48
|
-
status,
|
|
49
|
-
resourceType,
|
|
50
|
-
postData: request.postData() || undefined,
|
|
51
|
-
};
|
|
52
|
-
const id = trackEvent(context, {
|
|
53
|
-
type: 'network-request',
|
|
54
|
-
data: networkData,
|
|
55
|
-
});
|
|
56
|
-
getEventIdMap(context).set(id, { request });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { toArray, pipe, filter } from '@fxts/core';
|
|
2
|
-
import { parseGraphQLRequestFromHttp } from '../utils/graphql.js';
|
|
3
|
-
import { formatNetworkSummaryLine } from '../utils/networkFormat.js';
|
|
4
|
-
import { defineGroupingRule } from './grouping.js';
|
|
5
|
-
import { Ok } from '../utils/result.js';
|
|
6
|
-
import { hookNameSchema } from './schema.js';
|
|
7
|
-
import { normalizeUrlForGrouping } from './networkFilters.js';
|
|
8
|
-
import { getEventStore, isEventType } from './events.js';
|
|
9
|
-
const networkTrackingPreHook = {
|
|
10
|
-
name: hookNameSchema.enum['network-tracking-pre'],
|
|
11
|
-
handler: async (_ctx) => {
|
|
12
|
-
// Pre-hook now just acts as a marker, event consumption happens elsewhere
|
|
13
|
-
return Ok(undefined);
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
const networkTrackingPostHook = {
|
|
17
|
-
name: hookNameSchema.enum['network-tracking-post'],
|
|
18
|
-
handler: async (_ctx) => {
|
|
19
|
-
// Post-hook now just acts as a marker, network event collection happens automatically
|
|
20
|
-
return Ok(undefined);
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
export const networkTrackingHooks = {
|
|
24
|
-
pre: networkTrackingPreHook,
|
|
25
|
-
post: networkTrackingPostHook,
|
|
26
|
-
};
|
|
27
|
-
export const formatNetworkEvent = (event) => {
|
|
28
|
-
const { method, url, status, postData, setCookies } = event.data;
|
|
29
|
-
const summary = formatNetworkSummaryLine({ method, url, status, postData });
|
|
30
|
-
if (!setCookies || setCookies.length === 0)
|
|
31
|
-
return summary;
|
|
32
|
-
const names = setCookies
|
|
33
|
-
.map(cookie => {
|
|
34
|
-
const firstPart = cookie.split(';', 1)[0];
|
|
35
|
-
const [name] = firstPart.split('=', 1);
|
|
36
|
-
return name?.trim();
|
|
37
|
-
})
|
|
38
|
-
.filter((name) => !!name);
|
|
39
|
-
if (!names.length)
|
|
40
|
-
return summary;
|
|
41
|
-
return `${summary} | Set-Cookie keys: ${names.join(', ')}`;
|
|
42
|
-
};
|
|
43
|
-
const computeNetworkGroupKey = (event) => {
|
|
44
|
-
const method = (event.data.method || '').toUpperCase();
|
|
45
|
-
const baseUrl = normalizeUrlForGrouping(event.data.url);
|
|
46
|
-
const gql = parseGraphQLRequestFromHttp(event.data.method, event.data.url, {}, event.data.postData);
|
|
47
|
-
if (gql) {
|
|
48
|
-
const type = gql.operationType === 'unknown' ? 'operation' : gql.operationType;
|
|
49
|
-
const op = gql.operationName ? `${type} ${gql.operationName}` : type;
|
|
50
|
-
return `${method} ${baseUrl} [GraphQL: ${op}]`;
|
|
51
|
-
}
|
|
52
|
-
return `${method} ${baseUrl}`;
|
|
53
|
-
};
|
|
54
|
-
export const networkGroupingRule = defineGroupingRule({
|
|
55
|
-
match: (e) => e.type === 'network-request',
|
|
56
|
-
keyOf: e => computeNetworkGroupKey(e),
|
|
57
|
-
summaryOf: (first, run) => {
|
|
58
|
-
const key = computeNetworkGroupKey(first);
|
|
59
|
-
const count = run.length;
|
|
60
|
-
const firstStatus = first.data.status;
|
|
61
|
-
const allSameStatus = run.every(e => e.data.status === firstStatus);
|
|
62
|
-
return allSameStatus
|
|
63
|
-
? `${key} → ${firstStatus} (x${count})`
|
|
64
|
-
: `${key} (x${count})`;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
export const listNetworkEvents = (context) => pipe(context, getEventStore, store => store.events.values(), filter(isEventType('network-request')), toArray);
|