@percy/core 1.31.7 → 1.31.8

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/network.js CHANGED
@@ -348,6 +348,43 @@ export class Network {
348
348
  /* istanbul ignore if: race condition paranoia */
349
349
  if (!request) return;
350
350
 
351
+ // Log instrumentation for failed requests
352
+ let response = request.response;
353
+ let category, reason, details;
354
+
355
+ /* istanbul ignore next: rare edge case where loadingFailed is called with a response */
356
+ if (response) {
357
+ // Request has a response but failed (e.g., 404, 5xx)
358
+ if (response.status >= 500 && response.status < 600) {
359
+ category = 'asset_load_5xx';
360
+ reason = 'server_error';
361
+ } else if (!ALLOWED_STATUSES.includes(response.status)) {
362
+ category = 'asset_not_uploaded';
363
+ reason = 'disallowed_status';
364
+ }
365
+ if (category) {
366
+ details = {
367
+ url: request.url,
368
+ statusCode: response.status,
369
+ snapshot: this.meta.snapshot,
370
+ requestType: request.type
371
+ };
372
+ }
373
+ // Failed request without a response (network error)
374
+ } else if (event.errorText && event.errorText !== 'net::ERR_FAILED') {
375
+ category = 'asset_load_missing';
376
+ reason = 'network_error';
377
+ details = {
378
+ url: request.url,
379
+ errorText: event.errorText,
380
+ snapshot: this.meta.snapshot,
381
+ requestType: request.type
382
+ };
383
+ }
384
+ if (category) {
385
+ logAssetInstrumentation(this.log, category, reason, details);
386
+ }
387
+
351
388
  // If request was aborted, keep track of it as we need to cancel any in process callbacks for
352
389
  // such a request to avoid Invalid InterceptionId errors
353
390
  // Note: 404s also show up under ERR_ABORTED and not ERR_FAILED
@@ -377,6 +414,33 @@ export class Network {
377
414
  }
378
415
  }
379
416
 
417
+ // Logs asset instrumentation for failed/skipped asset loading
418
+ function logAssetInstrumentation(log, category, reason, details) {
419
+ const categoryMap = {
420
+ asset_load_5xx: '[ASSET_LOAD_5XX]',
421
+ asset_not_uploaded: '[ASSET_NOT_UPLOADED]',
422
+ asset_load_missing: '[ASSET_LOAD_MISSING]'
423
+ };
424
+ const messageMap = {
425
+ server_error: 'Server error response',
426
+ disallowed_status: 'Disallowed status code',
427
+ network_error: 'Network error',
428
+ disallowed_hostname: 'Disallowed hostname',
429
+ resource_too_large: 'Resource too large',
430
+ no_response: 'No response received',
431
+ empty_response: 'Empty response',
432
+ disallowed_resource_type: 'Disallowed resource type'
433
+ };
434
+ const prefix = categoryMap[category];
435
+ /* istanbul ignore next: fallback for unknown reason */
436
+ const message = messageMap[reason] || reason;
437
+ log.debug(`${prefix} ${message}`, {
438
+ ...details,
439
+ reason,
440
+ instrumentationCategory: category
441
+ });
442
+ }
443
+
380
444
  // Returns the normalized origin URL of a request
381
445
  function originURL(request) {
382
446
  return normalizeURL((request.redirectChain[0] || request).url);
@@ -399,6 +463,11 @@ async function sendResponseResource(network, request, session) {
399
463
  let resource = network.intercept.getResource(url, network.intercept.currentWidth);
400
464
  network.log.debug(`Handling request: ${url}`, meta);
401
465
  if (!(resource !== null && resource !== void 0 && resource.root) && hostnameMatches(disallowedHostnames, url)) {
466
+ logAssetInstrumentation(log, 'asset_not_uploaded', 'disallowed_hostname', {
467
+ url,
468
+ hostname: new URL(url).hostname,
469
+ snapshot: meta.snapshot
470
+ });
402
471
  log.debug('- Skipping disallowed hostname', meta);
403
472
  await send('Fetch.failRequest', {
404
473
  requestId: request.interceptId,
@@ -512,6 +581,11 @@ async function saveResponseResource(network, request, session) {
512
581
  let contentLength = (_response$headers = response.headers) === null || _response$headers === void 0 ? void 0 : _response$headers[Object.keys(response.headers).find(key => key.toLowerCase() === 'content-length')];
513
582
  contentLength = parseInt(contentLength);
514
583
  if (contentLength > MAX_RESOURCE_SIZE) {
584
+ logAssetInstrumentation(log, 'asset_not_uploaded', 'resource_too_large', {
585
+ url,
586
+ size: contentLength,
587
+ snapshot: meta.snapshot
588
+ });
515
589
  return log.debug('- Skipping resource larger than 25MB', meta);
516
590
  }
517
591
  let resource = network.intercept.getResource(url);
@@ -526,17 +600,51 @@ async function saveResponseResource(network, request, session) {
526
600
  // Don't rename the below log line as it is used in getting network logs in api
527
601
  /* istanbul ignore if: first check is a sanity check */
528
602
  if (!response) {
603
+ logAssetInstrumentation(log, 'asset_load_missing', 'no_response', {
604
+ url,
605
+ snapshot: meta.snapshot,
606
+ requestType: request.type
607
+ });
529
608
  return log.debug('- Skipping no response', meta);
530
609
  } else if (!shouldCapture) {
610
+ logAssetInstrumentation(log, 'asset_not_uploaded', 'disallowed_hostname', {
611
+ url,
612
+ hostname: new URL(url).hostname,
613
+ snapshot: meta.snapshot
614
+ });
531
615
  return log.debug('- Skipping remote resource', meta);
532
616
  } else if (!body.length) {
617
+ logAssetInstrumentation(log, 'asset_not_uploaded', 'empty_response', {
618
+ url,
619
+ snapshot: meta.snapshot
620
+ });
533
621
  return log.debug('- Skipping empty response', meta);
534
622
  } else if (body.length > MAX_RESOURCE_SIZE) {
623
+ logAssetInstrumentation(log, 'asset_not_uploaded', 'resource_too_large', {
624
+ url,
625
+ size: body.length,
626
+ snapshot: meta.snapshot
627
+ });
535
628
  log.debug('- Missing headers for the requested resource.', meta);
536
629
  return log.debug('- Skipping resource larger than 25MB', meta);
537
630
  } else if (!ALLOWED_STATUSES.includes(response.status)) {
631
+ /* istanbul ignore next: ternary branches tested separately */
632
+ const category = response.status >= 500 && response.status < 600 ? 'asset_load_5xx' : 'asset_not_uploaded';
633
+ /* istanbul ignore next: ternary branches tested separately */
634
+ const reason = response.status >= 500 && response.status < 600 ? 'server_error' : 'disallowed_status';
635
+ logAssetInstrumentation(log, category, reason, {
636
+ url,
637
+ statusCode: response.status,
638
+ snapshot: meta.snapshot,
639
+ requestType: request.type
640
+ });
538
641
  return log.debug(`- Skipping disallowed status [${response.status}]`, meta);
539
642
  } else if (!enableJavaScript && !ALLOWED_RESOURCES.includes(request.type)) {
643
+ logAssetInstrumentation(log, 'asset_not_uploaded', 'disallowed_resource_type', {
644
+ url,
645
+ resourceType: request.type,
646
+ snapshot: meta.snapshot
647
+ });
540
648
  return log.debug(`- Skipping disallowed resource type [${request.type}]`, meta);
541
649
  }
542
650
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.31.7",
3
+ "version": "1.31.8",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,12 +43,12 @@
43
43
  "test:types": "tsd"
44
44
  },
45
45
  "dependencies": {
46
- "@percy/client": "1.31.7",
47
- "@percy/config": "1.31.7",
48
- "@percy/dom": "1.31.7",
49
- "@percy/logger": "1.31.7",
50
- "@percy/monitoring": "1.31.7",
51
- "@percy/webdriver-utils": "1.31.7",
46
+ "@percy/client": "1.31.8",
47
+ "@percy/config": "1.31.8",
48
+ "@percy/dom": "1.31.8",
49
+ "@percy/logger": "1.31.8",
50
+ "@percy/monitoring": "1.31.8",
51
+ "@percy/webdriver-utils": "1.31.8",
52
52
  "content-disposition": "^0.5.4",
53
53
  "cross-spawn": "^7.0.3",
54
54
  "extract-zip": "^2.0.1",
@@ -61,5 +61,5 @@
61
61
  "ws": "^8.17.1",
62
62
  "yaml": "^2.4.1"
63
63
  },
64
- "gitHead": "a0b5ff9e0224d0ab4559f20170256320e91c6161"
64
+ "gitHead": "f5c77f4e541aca84042d7249105428c55303e809"
65
65
  }
package/types/index.d.ts CHANGED
@@ -59,9 +59,66 @@ interface CommonSnapshotOptions {
59
59
  scopeOptions?: ScopeOptions;
60
60
  browsers?: string[];
61
61
  }
62
+ // Region support for TypeScript
63
+ interface BoundingBox {
64
+ x: number;
65
+ y: number;
66
+ width: number;
67
+ height: number;
68
+ }
69
+
70
+ interface Padding {
71
+ top?: number;
72
+ left?: number;
73
+ right?: number;
74
+ bottom?: number;
75
+ }
76
+
77
+ interface ElementSelector {
78
+ boundingBox?: BoundingBox;
79
+ elementXpath?: string;
80
+ elementCSS?: string;
81
+ }
82
+
83
+ interface RegionConfiguration {
84
+ diffSensitivity?: number;
85
+ imageIgnoreThreshold?: number;
86
+ carouselsEnabled?: boolean;
87
+ bannersEnabled?: boolean;
88
+ adsEnabled?: boolean;
89
+ }
90
+
91
+ interface RegionAssertion {
92
+ diffIgnoreThreshold?: number;
93
+ }
94
+
95
+ export interface Region {
96
+ algorithm: string;
97
+ elementSelector: ElementSelector;
98
+ padding?: Padding;
99
+ configuration?: RegionConfiguration;
100
+ assertion?: RegionAssertion;
101
+ }
102
+
103
+ export interface CreateRegionOptions {
104
+ boundingBox?: BoundingBox;
105
+ elementXpath?: string;
106
+ elementCSS?: string;
107
+ padding?: Padding;
108
+ algorithm?: string;
109
+ diffSensitivity?: number;
110
+ imageIgnoreThreshold?: number;
111
+ carouselsEnabled?: boolean;
112
+ bannersEnabled?: boolean;
113
+ adsEnabled?: boolean;
114
+ diffIgnoreThreshold?: number;
115
+ }
116
+
117
+ export function createRegion(options: CreateRegionOptions): Region;
62
118
 
63
119
  export interface SnapshotOptions extends CommonSnapshotOptions {
64
120
  discovery?: DiscoveryOptions;
121
+ regions?: Region[];
65
122
  }
66
123
 
67
124
  type ClientEnvInfo = {