@percy/core 1.28.8 → 1.28.9-beta.0

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/discovery.js CHANGED
@@ -321,6 +321,9 @@ export function createDiscoveryQueue(percy) {
321
321
  // on start, launch the browser and run the queue
322
322
  .handle('start', async () => {
323
323
  cache = percy[RESOURCE_CACHE_KEY] = new Map();
324
+
325
+ // If browser.launch() fails it will get captured in
326
+ // *percy.start()
324
327
  await percy.browser.launch();
325
328
  queue.run();
326
329
  })
@@ -395,17 +398,34 @@ export function createDiscoveryQueue(percy) {
395
398
  throwOn: ['AbortError']
396
399
  });
397
400
  });
398
- }).handle('error', ({
401
+ }).handle('error', async ({
399
402
  name,
400
403
  meta
401
404
  }, error) => {
402
405
  if (error.name === 'AbortError' && queue.readyState < 3) {
403
406
  // only error about aborted snapshots when not closed
404
- percy.log.error('Received a duplicate snapshot, ' + `the previous snapshot was aborted: ${snapshotLogName(name, meta)}`, meta);
407
+ let errMsg = 'Received a duplicate snapshot, ' + `the previous snapshot was aborted: ${snapshotLogName(name, meta)}`;
408
+ percy.log.error(errMsg, {
409
+ snapshotLevel: true,
410
+ snapshotName: name
411
+ });
412
+ await percy.suggestionsForFix(errMsg, meta);
405
413
  } else {
406
414
  // log all other encountered errors
407
- percy.log.error(`Encountered an error taking snapshot: ${name}`, meta);
415
+ let errMsg = `Encountered an error taking snapshot: ${name}`;
416
+ percy.log.error(errMsg, meta);
408
417
  percy.log.error(error, meta);
418
+ let assetDiscoveryErrors = [{
419
+ message: errMsg,
420
+ meta
421
+ }, {
422
+ message: error === null || error === void 0 ? void 0 : error.message,
423
+ meta
424
+ }];
425
+ await percy.suggestionsForFix(assetDiscoveryErrors, {
426
+ snapshotLevel: true,
427
+ snapshotName: name
428
+ });
409
429
  }
410
430
  });
411
431
  }
package/dist/percy.js CHANGED
@@ -1,6 +1,20 @@
1
+ function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
2
+ function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
3
+ function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
4
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
5
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
6
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
7
+ function _classPrivateMethodGet(s, a, r) { return _assertClassBrand(a, s), r; }
8
+ function _classPrivateFieldGet2(e, t) { var r = _classPrivateFieldGet(t, e); return _classApplyDescriptorGet(e, r); }
9
+ function _classApplyDescriptorGet(e, t) { return t.get ? t.get.call(e) : t.value; }
10
+ function _classPrivateFieldSet(e, t, r) { var s = _classPrivateFieldGet(t, e); return _classApplyDescriptorSet(e, s, r), r; }
11
+ function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
12
+ function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
13
+ function _classApplyDescriptorSet(e, t, l) { if (t.set) t.set.call(e, l);else { if (!t.writable) throw new TypeError("attempted to set read only private field"); t.value = l; } }
1
14
  import PercyClient from '@percy/client';
2
15
  import PercyConfig from '@percy/config';
3
16
  import logger from '@percy/logger';
17
+ import { getProxy } from '@percy/client/utils';
4
18
  import Browser from './browser.js';
5
19
  import Pako from 'pako';
6
20
  import { base64encode, generatePromise, yieldAll, yieldTo, redactSecrets } from './utils.js';
@@ -12,12 +26,11 @@ import { WaitForJob } from './wait-for-job.js';
12
26
  // A Percy instance will create a new build when started, handle snapshot creation, asset discovery,
13
27
  // and resource uploads, and will finalize the build when stopped. Snapshots are processed
14
28
  // concurrently and the build is not finalized until all snapshots have been handled.
29
+ var _discovery = /*#__PURE__*/new WeakMap();
30
+ var _snapshots = /*#__PURE__*/new WeakMap();
31
+ var _displaySuggestionLogs = /*#__PURE__*/new WeakSet();
32
+ var _proxyEnabled = /*#__PURE__*/new WeakSet();
15
33
  export class Percy {
16
- log = logger('core');
17
- readyState = null;
18
- #discovery = null;
19
- #snapshots = null;
20
-
21
34
  // Static shortcut to create and start an instance in one call
22
35
  static async start(options) {
23
36
  let instance = new this(options);
@@ -52,11 +65,23 @@ export class Percy {
52
65
  projectType = null,
53
66
  // options such as `snapshot` and `discovery` that are valid Percy config
54
67
  // options which will become accessible via the `.config` property
55
- ...options
68
+ ..._options
56
69
  } = {}) {
57
70
  var _config$percy, _config$percy2;
71
+ _classPrivateMethodInitSpec(this, _proxyEnabled);
72
+ _classPrivateMethodInitSpec(this, _displaySuggestionLogs);
73
+ _defineProperty(this, "log", logger('core'));
74
+ _defineProperty(this, "readyState", null);
75
+ _classPrivateFieldInitSpec(this, _discovery, {
76
+ writable: true,
77
+ value: null
78
+ });
79
+ _classPrivateFieldInitSpec(this, _snapshots, {
80
+ writable: true,
81
+ value: null
82
+ });
58
83
  let config = PercyConfig.load({
59
- overrides: options,
84
+ overrides: _options,
60
85
  path: configFile
61
86
  });
62
87
  labels ?? (labels = (_config$percy = config.percy) === null || _config$percy === void 0 ? void 0 : _config$percy.labels);
@@ -64,6 +89,7 @@ export class Percy {
64
89
  this.config = config;
65
90
  if (testing) loglevel = 'silent';
66
91
  if (loglevel) this.loglevel(loglevel);
92
+ this.port = port;
67
93
  this.projectType = projectType;
68
94
  this.testing = testing ? {} : null;
69
95
  this.dryRun = !!testing || !!dryRun;
@@ -81,8 +107,8 @@ export class Percy {
81
107
  });
82
108
  if (server) this.server = createPercyServer(this, port);
83
109
  this.browser = new Browser(this);
84
- this.#discovery = createDiscoveryQueue(this);
85
- this.#snapshots = createSnapshotsQueue(this);
110
+ _classPrivateFieldSet(this, _discovery, createDiscoveryQueue(this));
111
+ _classPrivateFieldSet(this, _snapshots, createSnapshotsQueue(this));
86
112
 
87
113
  // generator methods are wrapped to autorun and return promises
88
114
  for (let m of ['start', 'stop', 'flush', 'idle', 'snapshot', 'upload']) {
@@ -135,10 +161,10 @@ export class Percy {
135
161
  let {
136
162
  concurrency
137
163
  } = this.config.discovery;
138
- this.#discovery.set({
164
+ _classPrivateFieldGet2(this, _discovery).set({
139
165
  concurrency
140
166
  });
141
- this.#snapshots.set({
167
+ _classPrivateFieldGet2(this, _snapshots).set({
142
168
  concurrency
143
169
  });
144
170
  return this.config;
@@ -154,9 +180,9 @@ export class Percy {
154
180
  this.log.warn('Notice: Percy collects CI logs for service improvement, stored for 30 days. Opt-out anytime with export PERCY_CLIENT_ERROR_LOGS=false');
155
181
  }
156
182
  // start the snapshots queue immediately when not delayed or deferred
157
- if (!this.delayUploads && !this.deferUploads) yield this.#snapshots.start();
183
+ if (!this.delayUploads && !this.deferUploads) yield _classPrivateFieldGet2(this, _snapshots).start();
158
184
  // do not start the discovery queue when not needed
159
- if (!this.skipDiscovery) yield this.#discovery.start();
185
+ if (!this.skipDiscovery) yield _classPrivateFieldGet2(this, _discovery).start();
160
186
  // start a local API server for SDK communication
161
187
  if (this.server) yield this.server.listen();
162
188
  if (this.projectType === 'web') {
@@ -174,16 +200,19 @@ export class Percy {
174
200
  var _this$server2;
175
201
  // on error, close any running server and end queues
176
202
  await ((_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.close());
177
- await this.#discovery.end();
178
- await this.#snapshots.end();
203
+ await _classPrivateFieldGet2(this, _discovery).end();
204
+ await _classPrivateFieldGet2(this, _snapshots).end();
179
205
 
180
206
  // mark this instance as closed unless aborting
181
207
  this.readyState = error.name !== 'AbortError' ? 3 : null;
182
208
 
183
209
  // throw an easier-to-understand error when the port is in use
184
210
  if (error.code === 'EADDRINUSE') {
185
- throw new Error('Percy is already running or the port is in use');
211
+ let errMsg = `Percy is already running or the port ${this.port} is in use`;
212
+ await this.suggestionsForFix(errMsg);
213
+ throw new Error(errMsg);
186
214
  } else {
215
+ await this.suggestionsForFix(error.message);
187
216
  throw error;
188
217
  }
189
218
  }
@@ -191,8 +220,8 @@ export class Percy {
191
220
 
192
221
  // Resolves once snapshot and upload queues are idle
193
222
  async *idle() {
194
- yield* this.#discovery.idle();
195
- yield* this.#snapshots.idle();
223
+ yield* _classPrivateFieldGet2(this, _discovery).idle();
224
+ yield* _classPrivateFieldGet2(this, _snapshots).idle();
196
225
  }
197
226
 
198
227
  // Wait for currently queued snapshots then run and wait for resulting uploads
@@ -205,13 +234,13 @@ export class Percy {
205
234
  yield new Promise(r => setImmediate(r));
206
235
 
207
236
  // flush and log progress for discovery before snapshots
208
- if (!this.skipDiscovery && this.#discovery.size) {
209
- if (options) yield* yieldAll(options.map(o => this.#discovery.process(o)));else yield* this.#discovery.flush(size => callback === null || callback === void 0 ? void 0 : callback('Processing', size));
237
+ if (!this.skipDiscovery && _classPrivateFieldGet2(this, _discovery).size) {
238
+ if (options) yield* yieldAll(options.map(o => _classPrivateFieldGet2(this, _discovery).process(o)));else yield* _classPrivateFieldGet2(this, _discovery).flush(size => callback === null || callback === void 0 ? void 0 : callback('Processing', size));
210
239
  }
211
240
 
212
241
  // flush and log progress for snapshot uploads
213
- if (!this.skipUploads && this.#snapshots.size) {
214
- if (options) yield* yieldAll(options.map(o => this.#snapshots.process(o)));else yield* this.#snapshots.flush(size => callback === null || callback === void 0 ? void 0 : callback('Uploading', size));
242
+ if (!this.skipUploads && _classPrivateFieldGet2(this, _snapshots).size) {
243
+ if (options) yield* yieldAll(options.map(o => _classPrivateFieldGet2(this, _snapshots).process(o)));else yield* _classPrivateFieldGet2(this, _snapshots).flush(size => callback === null || callback === void 0 ? void 0 : callback('Uploading', size));
215
244
  }
216
245
  }
217
246
 
@@ -230,8 +259,8 @@ export class Percy {
230
259
 
231
260
  // close queues asap
232
261
  if (force) {
233
- this.#discovery.close(true);
234
- this.#snapshots.close(true);
262
+ _classPrivateFieldGet2(this, _discovery).close(true);
263
+ _classPrivateFieldGet2(this, _snapshots).close(true);
235
264
  }
236
265
 
237
266
  // already stopping
@@ -256,14 +285,14 @@ export class Percy {
256
285
  }
257
286
 
258
287
  // if dry-running, log the total number of snapshots
259
- if (this.dryRun && this.#snapshots.size) {
260
- this.log.info(info('Found', this.#snapshots.size));
288
+ if (this.dryRun && _classPrivateFieldGet2(this, _snapshots).size) {
289
+ this.log.info(info('Found', _classPrivateFieldGet2(this, _snapshots).size));
261
290
  }
262
291
 
263
292
  // close server and end queues
264
293
  await ((_this$server3 = this.server) === null || _this$server3 === void 0 ? void 0 : _this$server3.close());
265
- await this.#discovery.end();
266
- await this.#snapshots.end();
294
+ await _classPrivateFieldGet2(this, _discovery).end();
295
+ await _classPrivateFieldGet2(this, _snapshots).end();
267
296
 
268
297
  // mark instance as stopped
269
298
  this.readyState = 3;
@@ -271,6 +300,9 @@ export class Percy {
271
300
  this.log.error(err);
272
301
  throw err;
273
302
  } finally {
303
+ // This issue doesn't comes under regular error logs,
304
+ // it's detected if we just and stop percy server
305
+ await this.checkForNoSnapshotCommandError();
274
306
  await this.sendBuildLogs();
275
307
  }
276
308
  }
@@ -323,7 +355,7 @@ export class Percy {
323
355
  }
324
356
 
325
357
  // gather snapshots and discover snapshot resources
326
- yield* discoverSnapshotResources(this.#discovery, {
358
+ yield* discoverSnapshotResources(_classPrivateFieldGet2(this, _discovery), {
327
359
  skipDiscovery: this.skipDiscovery,
328
360
  dryRun: this.dryRun,
329
361
  snapshots: yield* gatherSnapshots(options, {
@@ -343,7 +375,7 @@ export class Percy {
343
375
  });
344
376
  }
345
377
  // push each finished snapshot to the snapshots queue
346
- this.#snapshots.push(snapshot);
378
+ _classPrivateFieldGet2(this, _snapshots).push(snapshot);
347
379
  });
348
380
  } finally {
349
381
  var _server;
@@ -403,9 +435,11 @@ export class Percy {
403
435
  // return an async generator to allow cancelation
404
436
  return async function* () {
405
437
  try {
406
- return yield* yieldTo(this.#snapshots.push(options));
438
+ return yield* yieldTo(_classPrivateFieldGet2(this, _snapshots).push(options));
407
439
  } catch (error) {
408
- this.#snapshots.cancel(options);
440
+ _classPrivateFieldGet2(this, _snapshots).cancel(options);
441
+ // Detecting and suggesting fix for errors;
442
+ await this.suggestionsForFix(error.message);
409
443
  throw error;
410
444
  }
411
445
  }.call(this);
@@ -437,6 +471,48 @@ export class Percy {
437
471
  if (syncMode) options.sync = syncMode;
438
472
  return syncMode;
439
473
  }
474
+
475
+ // This specific error will be hard coded
476
+ async checkForNoSnapshotCommandError() {
477
+ let isPercyStarted = false;
478
+ let containsSnapshotTaken = false;
479
+ logger.query(item => {
480
+ var _item$message, _item$message2, _item$message3;
481
+ isPercyStarted || (isPercyStarted = item === null || item === void 0 ? void 0 : (_item$message = item.message) === null || _item$message === void 0 ? void 0 : _item$message.includes('Percy has started'));
482
+ containsSnapshotTaken || (containsSnapshotTaken = item === null || item === void 0 ? void 0 : (_item$message2 = item.message) === null || _item$message2 === void 0 ? void 0 : _item$message2.includes('Snapshot taken'));
483
+
484
+ // This case happens when you directly upload it using cli-upload
485
+ containsSnapshotTaken || (containsSnapshotTaken = item === null || item === void 0 ? void 0 : (_item$message3 = item.message) === null || _item$message3 === void 0 ? void 0 : _item$message3.includes('Snapshot uploaded'));
486
+ return item;
487
+ });
488
+ if (isPercyStarted && !containsSnapshotTaken) {
489
+ // This is the case for No snapshot command called
490
+ _classPrivateMethodGet(this, _displaySuggestionLogs, _displaySuggestionLogs2).call(this, [{
491
+ failure_reason: 'Snapshot command was not called',
492
+ reason_message: 'Snapshot Command was not called. please check your CI for errors',
493
+ suggestion: 'Try using percy snapshot command to take snapshots',
494
+ reference_doc_link: ['https://www.browserstack.com/docs/percy/take-percy-snapshots/']
495
+ }]);
496
+ }
497
+ }
498
+ async suggestionsForFix(errors, options = {}) {
499
+ try {
500
+ const suggestionResponse = await this.client.getErrorAnalysis(errors);
501
+ _classPrivateMethodGet(this, _displaySuggestionLogs, _displaySuggestionLogs2).call(this, suggestionResponse, options);
502
+ } catch (e) {
503
+ // Common error code for Proxy issues
504
+ const PROXY_CODES = ['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH'];
505
+ if (!!e.code && PROXY_CODES.includes(e.code)) {
506
+ // This can be due to proxy issue
507
+ this.log.error('percy.io might not be reachable, check network connection, proxy and ensure that percy.io is whitelisted.');
508
+ if (!_classPrivateMethodGet(this, _proxyEnabled, _proxyEnabled2).call(this)) {
509
+ this.log.error('If inside a proxied envirnment, please configure the following environment variables: HTTP_PROXY, [ and optionally HTTPS_PROXY if you need it ]. Refer to our documentation for more details');
510
+ }
511
+ }
512
+ this.log.error('Unable to analyze error logs');
513
+ this.log.debug(e);
514
+ }
515
+ }
440
516
  async sendBuildLogs() {
441
517
  if (!process.env.PERCY_TOKEN) return;
442
518
  try {
@@ -444,6 +520,7 @@ export class Percy {
444
520
  const logsObject = {
445
521
  clilogs: logger.query(log => !['ci'].includes(log.debug))
446
522
  };
523
+
447
524
  // Only add CI logs if not disabled voluntarily.
448
525
  const sendCILogs = process.env.PERCY_CLIENT_ERROR_LOGS !== 'false';
449
526
  if (sendCILogs) {
@@ -467,4 +544,32 @@ export class Percy {
467
544
  }
468
545
  }
469
546
  }
547
+ function _displaySuggestionLogs2(suggestions, options = {}) {
548
+ if (!(suggestions !== null && suggestions !== void 0 && suggestions.length)) return;
549
+ suggestions.forEach(item => {
550
+ const failure = item === null || item === void 0 ? void 0 : item.failure_reason;
551
+ const failureReason = item === null || item === void 0 ? void 0 : item.reason_message;
552
+ const suggestion = item === null || item === void 0 ? void 0 : item.suggestion;
553
+ const referenceDocLinks = item === null || item === void 0 ? void 0 : item.reference_doc_link;
554
+ if (options !== null && options !== void 0 && options.snapshotLevel) {
555
+ this.log.warn(`Detected erorr for Snapshot: ${options === null || options === void 0 ? void 0 : options.snapshotName}`);
556
+ } else {
557
+ this.log.warn('Detected error for percy build');
558
+ }
559
+ this.log.warn(`Failure: ${failure}`);
560
+ this.log.warn(`Failure Reason: ${failureReason}`);
561
+ this.log.warn(`Suggestion: ${suggestion}`);
562
+ if ((referenceDocLinks === null || referenceDocLinks === void 0 ? void 0 : referenceDocLinks.length) > 0) {
563
+ this.log.warn('Refer to the below Doc Links for the same');
564
+ referenceDocLinks === null || referenceDocLinks === void 0 ? void 0 : referenceDocLinks.forEach(_docLink => {
565
+ this.log.warn(`* ${_docLink}`);
566
+ });
567
+ }
568
+ });
569
+ }
570
+ function _proxyEnabled2() {
571
+ return !!(getProxy({
572
+ protocol: 'https:'
573
+ }) || getProxy({}));
574
+ }
470
575
  export default Percy;
package/dist/snapshot.js CHANGED
@@ -234,6 +234,7 @@ export async function handleSyncJob(jobPromise, percy, type) {
234
234
  data = await percy.client.getComparisonDetails(id);
235
235
  }
236
236
  } catch (e) {
237
+ await percy.suggestionsForFix(e.message);
237
238
  data = {
238
239
  error: e.message
239
240
  };
@@ -453,7 +454,7 @@ export function createSnapshotsQueue(percy) {
453
454
  };
454
455
  })
455
456
  // handle possible build errors returned by the API
456
- .handle('error', (snapshot, error) => {
457
+ .handle('error', async (snapshot, error) => {
457
458
  var _error$response, _error$response2, _error$response2$body;
458
459
  let result = {
459
460
  ...snapshot,
@@ -478,12 +479,29 @@ export function createSnapshotsQueue(percy) {
478
479
  let duplicate = (errors === null || errors === void 0 ? void 0 : errors.length) > 1 && errors[1].detail.includes('must be unique');
479
480
  if (duplicate) {
480
481
  if (process.env.PERCY_IGNORE_DUPLICATES !== 'true') {
481
- percy.log.warn(`Ignored duplicate snapshot. ${errors[1].detail}`);
482
+ let errMsg = `Ignored duplicate snapshot. ${errors[1].detail}`;
483
+ percy.log.warn(errMsg);
484
+ await percy.suggestionsForFix(errMsg, {
485
+ snapshotLevel: true,
486
+ snapshotName: name
487
+ });
482
488
  }
483
489
  return result;
484
490
  }
485
- percy.log.error(`Encountered an error uploading snapshot: ${name}`, meta);
491
+ let errMsg = `Encountered an error uploading snapshot: ${name}`;
492
+ percy.log.error(errMsg, meta);
486
493
  percy.log.error(error, meta);
494
+ let snapshotErrors = [{
495
+ message: errMsg,
496
+ meta
497
+ }, {
498
+ message: error === null || error === void 0 ? void 0 : error.message,
499
+ meta
500
+ }];
501
+ await percy.suggestionsForFix(snapshotErrors, {
502
+ snapshotLevel: true,
503
+ snapshotName: name
504
+ });
487
505
  if (snapshot.sync) snapshot.reject(error);
488
506
  return result;
489
507
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.28.8",
3
+ "version": "1.28.9-beta.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "publishConfig": {
11
11
  "access": "public",
12
- "tag": "latest"
12
+ "tag": "beta"
13
13
  },
14
14
  "engines": {
15
15
  "node": ">=14"
@@ -43,11 +43,11 @@
43
43
  "test:types": "tsd"
44
44
  },
45
45
  "dependencies": {
46
- "@percy/client": "1.28.8",
47
- "@percy/config": "1.28.8",
48
- "@percy/dom": "1.28.8",
49
- "@percy/logger": "1.28.8",
50
- "@percy/webdriver-utils": "1.28.8",
46
+ "@percy/client": "1.28.9-beta.0",
47
+ "@percy/config": "1.28.9-beta.0",
48
+ "@percy/dom": "1.28.9-beta.0",
49
+ "@percy/logger": "1.28.9-beta.0",
50
+ "@percy/webdriver-utils": "1.28.9-beta.0",
51
51
  "content-disposition": "^0.5.4",
52
52
  "cross-spawn": "^7.0.3",
53
53
  "extract-zip": "^2.0.1",
@@ -60,5 +60,5 @@
60
60
  "ws": "^8.17.1",
61
61
  "yaml": "^2.4.1"
62
62
  },
63
- "gitHead": "1b93761a01b608afadf8d4efccacc11f925396bd"
63
+ "gitHead": "a1114f1e18518012f48756c9558a8e7895d2b3a9"
64
64
  }