@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.
Files changed (89) hide show
  1. package/cli-wrapper.js +15 -14
  2. package/cli.js +1 -1
  3. package/config.d.ts +11 -6
  4. package/index.d.ts +7 -5
  5. package/index.js +1 -1
  6. package/package.json +34 -57
  7. package/LICENSE +0 -202
  8. package/lib/browserContextFactory.js +0 -326
  9. package/lib/browserServerBackend.js +0 -84
  10. package/lib/config.js +0 -286
  11. package/lib/context.js +0 -309
  12. package/lib/extension/cdpRelay.js +0 -346
  13. package/lib/extension/extensionContextFactory.js +0 -56
  14. package/lib/frameworkPatterns.js +0 -35
  15. package/lib/hooks/antiBotDetectionHook.js +0 -171
  16. package/lib/hooks/core.js +0 -144
  17. package/lib/hooks/eventConsumer.js +0 -52
  18. package/lib/hooks/events.js +0 -42
  19. package/lib/hooks/formatToolCallEvent.js +0 -16
  20. package/lib/hooks/frameworkStateHook.js +0 -182
  21. package/lib/hooks/grouping.js +0 -72
  22. package/lib/hooks/jsonLdDetectionHook.js +0 -175
  23. package/lib/hooks/networkFilters.js +0 -82
  24. package/lib/hooks/networkSetup.js +0 -59
  25. package/lib/hooks/networkTrackingHook.js +0 -67
  26. package/lib/hooks/pageHeightHook.js +0 -75
  27. package/lib/hooks/registry.js +0 -42
  28. package/lib/hooks/requireTabHook.js +0 -26
  29. package/lib/hooks/schema.js +0 -89
  30. package/lib/hooks/waitHook.js +0 -33
  31. package/lib/index.js +0 -39
  32. package/lib/mcp/inProcessTransport.js +0 -72
  33. package/lib/mcp/proxyBackend.js +0 -115
  34. package/lib/mcp/server.js +0 -86
  35. package/lib/mcp/tool.js +0 -38
  36. package/lib/mcp/transport.js +0 -181
  37. package/lib/playwrightTransformer.js +0 -497
  38. package/lib/program.js +0 -110
  39. package/lib/response.js +0 -186
  40. package/lib/sessionLog.js +0 -121
  41. package/lib/tab.js +0 -249
  42. package/lib/tools/common.js +0 -55
  43. package/lib/tools/console.js +0 -33
  44. package/lib/tools/dialogs.js +0 -47
  45. package/lib/tools/evaluate.js +0 -53
  46. package/lib/tools/extractFrameworkState.js +0 -214
  47. package/lib/tools/files.js +0 -45
  48. package/lib/tools/form.js +0 -57
  49. package/lib/tools/getSnapshot.js +0 -37
  50. package/lib/tools/getVisibleHtml.js +0 -52
  51. package/lib/tools/install.js +0 -51
  52. package/lib/tools/keyboard.js +0 -78
  53. package/lib/tools/mouse.js +0 -99
  54. package/lib/tools/navigate.js +0 -70
  55. package/lib/tools/network.js +0 -123
  56. package/lib/tools/networkDetail.js +0 -229
  57. package/lib/tools/networkSearch/bodySearch.js +0 -147
  58. package/lib/tools/networkSearch/grouping.js +0 -28
  59. package/lib/tools/networkSearch/helpers.js +0 -32
  60. package/lib/tools/networkSearch/searchHtml.js +0 -67
  61. package/lib/tools/networkSearch/types.js +0 -1
  62. package/lib/tools/networkSearch/urlSearch.js +0 -82
  63. package/lib/tools/networkSearch.js +0 -268
  64. package/lib/tools/pdf.js +0 -40
  65. package/lib/tools/repl.js +0 -402
  66. package/lib/tools/screenshot.js +0 -79
  67. package/lib/tools/scroll.js +0 -126
  68. package/lib/tools/snapshot.js +0 -144
  69. package/lib/tools/tabs.js +0 -59
  70. package/lib/tools/tool.js +0 -33
  71. package/lib/tools/utils.js +0 -74
  72. package/lib/tools/wait.js +0 -55
  73. package/lib/tools.js +0 -67
  74. package/lib/utils/adBlockFilter.js +0 -87
  75. package/lib/utils/codegen.js +0 -51
  76. package/lib/utils/extensionPath.js +0 -10
  77. package/lib/utils/fileUtils.js +0 -36
  78. package/lib/utils/graphql.js +0 -258
  79. package/lib/utils/guid.js +0 -22
  80. package/lib/utils/httpServer.js +0 -39
  81. package/lib/utils/log.js +0 -21
  82. package/lib/utils/manualPromise.js +0 -111
  83. package/lib/utils/networkFormat.js +0 -12
  84. package/lib/utils/package.js +0 -20
  85. package/lib/utils/result.js +0 -2
  86. package/lib/utils/sanitizeHtml.js +0 -98
  87. package/lib/utils/truncate.js +0 -103
  88. package/lib/utils/withTimeout.js +0 -7
  89. 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
- };
@@ -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);