Skip to content

Commit f4a4741

Browse files
feat: normalize query string for playwright engine
This PR introduces query string improvements for Playwright tests, to reduce the number of distinct metrics a test run can generate and help users get more meaninful data out of a test. - `stripQueryString`: as seen in the `metrics-by-endpoint` http plugin, query strings can now be removed — it defaults to `false` - `normalizeQueryString`: defaults to `true`, prevents too many distinct metrics, can be explicity set to `normalizeQueryString: false` to disable this behavior - For numeric values, it converts `/page?id=123` to `/page?id=NUMBER` - For string values, it converts `/page?plan=premium` to `/page?plan=STRING` - When mixed values are present, it converts `/page?id=123&plan=premium` to `/page?id=NUMBER&plan=STRING`
1 parent 7256c3f commit f4a4741

File tree

5 files changed

+149
-3
lines changed

5 files changed

+149
-3
lines changed

packages/artillery-engine-playwright/index.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ class PlaywrightEngine {
3636
this.showAllPageMetrics =
3737
typeof script.config.engines.playwright.showAllPageMetrics !==
3838
'undefined';
39+
this.stripQueryString =
40+
script.config.engines.playwright.stripQueryString || false;
41+
this.normalizeQueryString =
42+
script.config.engines.playwright.normalizeQueryString !== false;
3943

4044
this.useSeparateBrowserPerVU =
4145
typeof script.config.engines.playwright.useSeparateBrowserPerVU ===
@@ -97,8 +101,49 @@ class PlaywrightEngine {
97101

98102
const self = this;
99103

104+
function normalizeUrl(url) {
105+
if (!url) return url;
106+
107+
try {
108+
const urlObj = new URL(url);
109+
110+
// Strip query string if enabled
111+
if (self.stripQueryString) {
112+
return urlObj.origin + urlObj.pathname;
113+
}
114+
115+
// Normalize query string if enabled
116+
if (self.normalizeQueryString && urlObj.search) {
117+
const params = new URLSearchParams(urlObj.search);
118+
const normalizedParams = new URLSearchParams();
119+
120+
for (const [key, value] of params) {
121+
// Replace numeric values with NUMBER, all other values with STRING
122+
const normalizedValue = /^-?\d+(\.\d+)?$/.test(value)
123+
? 'NUMBER'
124+
: 'STRING';
125+
normalizedParams.append(key, normalizedValue);
126+
}
127+
128+
return (
129+
urlObj.origin +
130+
urlObj.pathname +
131+
(normalizedParams.toString()
132+
? '?' + normalizedParams.toString()
133+
: '')
134+
);
135+
}
136+
137+
return url;
138+
} catch (e) {
139+
// If URL parsing fails, return original URL
140+
debug('Failed to parse URL for normalization:', e);
141+
return url;
142+
}
143+
}
144+
100145
function getName(url) {
101-
return self.aggregateByName && spec.name ? spec.name : url;
146+
return self.aggregateByName && spec.name ? spec.name : normalizeUrl(url);
102147
}
103148

104149
const step = async (stepName, userActions) => {

packages/artillery-engine-playwright/test/fixtures/processor.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,26 @@ async function playwrightFunctionWithFailure(page, _vuContext, events, test) {
4444
events.emit('counter', 'custom_emitter', 1);
4545
}
4646

47+
async function urlNormalizationTest(page, _vuContext, events, test) {
48+
const testUrls = [
49+
'/docs?id=123',
50+
'/docs?id=456',
51+
'/docs?plan=team',
52+
'/docs?plan=business',
53+
'/docs?id=789&plan=team',
54+
'/docs?id=999&plan=business'
55+
];
56+
57+
for (const url of testUrls) {
58+
await test.step(`visit_${url}`, async () => {
59+
await retryGoingToPage(page, url);
60+
await page.waitForTimeout(100);
61+
});
62+
}
63+
}
64+
4765
module.exports = {
4866
artilleryPlaywrightFunction,
49-
playwrightFunctionWithFailure
67+
playwrightFunctionWithFailure,
68+
urlNormalizationTest
5069
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
config:
2+
target: https://www.artillery.io
3+
phases:
4+
- duration: 5
5+
arrivalRate: 1
6+
engines:
7+
playwright:
8+
extendedMetrics: true
9+
stripQueryString: false
10+
normalizeQueryString: true
11+
processor: "./processor.js"
12+
13+
scenarios:
14+
- name: test-url-normalization
15+
engine: playwright
16+
testFunction: urlNormalizationTest

packages/artillery-engine-playwright/test/index.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,57 @@ test('playwright typescript test fails and has correct vu count when expectation
284284
);
285285
}
286286
});
287+
288+
test('playwright url normalization works correctly', async (t) => {
289+
const output =
290+
await $`../artillery/bin/run run ./test/fixtures/pw-url-normalization.yml --output ${playwrightOutput}`;
291+
292+
t.equal(
293+
output.exitCode,
294+
0,
295+
`should have exit code 0, got ${output.exitCode}`
296+
);
297+
298+
const jsonReport = JSON.parse(fs.readFileSync(playwrightOutput, 'utf8'));
299+
const counters = jsonReport.aggregate.counters;
300+
const summaries = jsonReport.aggregate.summaries;
301+
302+
// Check that numeric parameters were normalized
303+
const docsIdMetricKey = `browser.page.domcontentloaded.${TEST_URL}docs?id=NUMBER`;
304+
t.ok(
305+
counters[docsIdMetricKey] >= 2,
306+
'should have normalized /docs?id=123 and /docs?id=456 to /docs?id=NUMBER'
307+
);
308+
309+
// Check that string parameters are normalized
310+
const docsPlanMetricKey = `browser.page.domcontentloaded.${TEST_URL}docs?plan=STRING`;
311+
t.ok(
312+
counters[docsPlanMetricKey] >= 2,
313+
'should have normalized /docs?plan=team and /docs?plan=business to /docs?plan=STRING'
314+
);
315+
316+
// Check mixed parameters (both numeric and string)
317+
const mixedMetricKey = `browser.page.domcontentloaded.${TEST_URL}docs?id=NUMBER&plan=STRING`;
318+
t.ok(
319+
counters[mixedMetricKey] >= 2,
320+
'should have normalized /docs?id=789&plan=team and /docs?id=999&plan=business to /docs?id=NUMBER&plan=STRING'
321+
);
322+
323+
// Ensure we don’t have the non-normalized metrics
324+
t.notOk(
325+
counters[`browser.page.domcontentloaded.${TEST_URL}docs?id=123`],
326+
'should not have metric for specific id=123'
327+
);
328+
t.notOk(
329+
counters[`browser.page.domcontentloaded.${TEST_URL}docs?id=456`],
330+
'should not have metric for specific id=456'
331+
);
332+
t.notOk(
333+
counters[`browser.page.domcontentloaded.${TEST_URL}docs?plan=team`],
334+
'should not have metric for specific plan=team'
335+
);
336+
t.notOk(
337+
counters[`browser.page.domcontentloaded.${TEST_URL}docs?plan=business`],
338+
'should not have metric for specific plan=business'
339+
);
340+
});

packages/types/schema/engines/playwright.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const PlaywrightConfigSchema = Joi.object({
3333
'Default maximum navigation time (in seconds) for Playwright navigation methods, like `page.goto()`.\nhttps://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout'
3434
),
3535
testIdAttribute: Joi.string()
36-
.meta({ title: 'Test ID Attribute'})
36+
.meta({ title: 'Test ID Attribute' })
3737
.description(
3838
'When set, changes the attribute used by locator `page.getByTestId` in Playwright. \n https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-test-id'
3939
),
@@ -61,7 +61,19 @@ const PlaywrightConfigSchema = Joi.object({
6161
.meta({ title: 'Use a separate browser process for each VU' })
6262
.description(
6363
'If enabled, a new browser process will be created for each VU. By default Artillery uses new browser contexts for new VUs.\nWARNING: Using this option is discouraged as it will increase CPU/memory usage of your tests.\nhttps://www.artillery.io/docs/reference/engines/playwright#configuration'
64+
),
65+
stripQueryString: artilleryBooleanOrString
66+
.meta({ title: 'Strip Query String' })
67+
.description(
68+
'Strip query strings from page URLs when generating metrics. Similar to the metrics-by-endpoint plugin for HTTP tests.'
69+
)
70+
.default(false),
71+
normalizeQueryString: artilleryBooleanOrString
72+
.meta({ title: 'Normalize Query String' })
73+
.description(
74+
'Replace parameter values in query strings with placeholders when generating metrics. Numeric values become NUMBER, string values become STRING. For example, /page?id=123 becomes /page?id=NUMBER and /page?name=john becomes /page?name=STRING'
6475
)
76+
.default(true)
6577
});
6678

6779
module.exports = {

0 commit comments

Comments
 (0)