@percy/core 1.28.8-beta.6 → 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,17 +65,31 @@ 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
- var _config$percy;
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
- deferUploads ?? (deferUploads = (_config$percy = config.percy) === null || _config$percy === void 0 ? void 0 : _config$percy.deferUploads);
87
+ labels ?? (labels = (_config$percy = config.percy) === null || _config$percy === void 0 ? void 0 : _config$percy.labels);
88
+ deferUploads ?? (deferUploads = (_config$percy2 = config.percy) === null || _config$percy2 === void 0 ? void 0 : _config$percy2.deferUploads);
63
89
  this.config = config;
64
90
  if (testing) loglevel = 'silent';
65
91
  if (loglevel) this.loglevel(loglevel);
92
+ this.port = port;
66
93
  this.projectType = projectType;
67
94
  this.testing = testing ? {} : null;
68
95
  this.dryRun = !!testing || !!dryRun;
@@ -80,8 +107,8 @@ export class Percy {
80
107
  });
81
108
  if (server) this.server = createPercyServer(this, port);
82
109
  this.browser = new Browser(this);
83
- this.#discovery = createDiscoveryQueue(this);
84
- this.#snapshots = createSnapshotsQueue(this);
110
+ _classPrivateFieldSet(this, _discovery, createDiscoveryQueue(this));
111
+ _classPrivateFieldSet(this, _snapshots, createSnapshotsQueue(this));
85
112
 
86
113
  // generator methods are wrapped to autorun and return promises
87
114
  for (let m of ['start', 'stop', 'flush', 'idle', 'snapshot', 'upload']) {
@@ -134,10 +161,10 @@ export class Percy {
134
161
  let {
135
162
  concurrency
136
163
  } = this.config.discovery;
137
- this.#discovery.set({
164
+ _classPrivateFieldGet2(this, _discovery).set({
138
165
  concurrency
139
166
  });
140
- this.#snapshots.set({
167
+ _classPrivateFieldGet2(this, _snapshots).set({
141
168
  concurrency
142
169
  });
143
170
  return this.config;
@@ -153,9 +180,9 @@ export class Percy {
153
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');
154
181
  }
155
182
  // start the snapshots queue immediately when not delayed or deferred
156
- if (!this.delayUploads && !this.deferUploads) yield this.#snapshots.start();
183
+ if (!this.delayUploads && !this.deferUploads) yield _classPrivateFieldGet2(this, _snapshots).start();
157
184
  // do not start the discovery queue when not needed
158
- if (!this.skipDiscovery) yield this.#discovery.start();
185
+ if (!this.skipDiscovery) yield _classPrivateFieldGet2(this, _discovery).start();
159
186
  // start a local API server for SDK communication
160
187
  if (this.server) yield this.server.listen();
161
188
  if (this.projectType === 'web') {
@@ -173,16 +200,19 @@ export class Percy {
173
200
  var _this$server2;
174
201
  // on error, close any running server and end queues
175
202
  await ((_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.close());
176
- await this.#discovery.end();
177
- await this.#snapshots.end();
203
+ await _classPrivateFieldGet2(this, _discovery).end();
204
+ await _classPrivateFieldGet2(this, _snapshots).end();
178
205
 
179
206
  // mark this instance as closed unless aborting
180
207
  this.readyState = error.name !== 'AbortError' ? 3 : null;
181
208
 
182
209
  // throw an easier-to-understand error when the port is in use
183
210
  if (error.code === 'EADDRINUSE') {
184
- 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);
185
214
  } else {
215
+ await this.suggestionsForFix(error.message);
186
216
  throw error;
187
217
  }
188
218
  }
@@ -190,8 +220,8 @@ export class Percy {
190
220
 
191
221
  // Resolves once snapshot and upload queues are idle
192
222
  async *idle() {
193
- yield* this.#discovery.idle();
194
- yield* this.#snapshots.idle();
223
+ yield* _classPrivateFieldGet2(this, _discovery).idle();
224
+ yield* _classPrivateFieldGet2(this, _snapshots).idle();
195
225
  }
196
226
 
197
227
  // Wait for currently queued snapshots then run and wait for resulting uploads
@@ -204,13 +234,13 @@ export class Percy {
204
234
  yield new Promise(r => setImmediate(r));
205
235
 
206
236
  // flush and log progress for discovery before snapshots
207
- if (!this.skipDiscovery && this.#discovery.size) {
208
- 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));
209
239
  }
210
240
 
211
241
  // flush and log progress for snapshot uploads
212
- if (!this.skipUploads && this.#snapshots.size) {
213
- 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));
214
244
  }
215
245
  }
216
246
 
@@ -229,8 +259,8 @@ export class Percy {
229
259
 
230
260
  // close queues asap
231
261
  if (force) {
232
- this.#discovery.close(true);
233
- this.#snapshots.close(true);
262
+ _classPrivateFieldGet2(this, _discovery).close(true);
263
+ _classPrivateFieldGet2(this, _snapshots).close(true);
234
264
  }
235
265
 
236
266
  // already stopping
@@ -255,14 +285,14 @@ export class Percy {
255
285
  }
256
286
 
257
287
  // if dry-running, log the total number of snapshots
258
- if (this.dryRun && this.#snapshots.size) {
259
- 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));
260
290
  }
261
291
 
262
292
  // close server and end queues
263
293
  await ((_this$server3 = this.server) === null || _this$server3 === void 0 ? void 0 : _this$server3.close());
264
- await this.#discovery.end();
265
- await this.#snapshots.end();
294
+ await _classPrivateFieldGet2(this, _discovery).end();
295
+ await _classPrivateFieldGet2(this, _snapshots).end();
266
296
 
267
297
  // mark instance as stopped
268
298
  this.readyState = 3;
@@ -270,6 +300,9 @@ export class Percy {
270
300
  this.log.error(err);
271
301
  throw err;
272
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();
273
306
  await this.sendBuildLogs();
274
307
  }
275
308
  }
@@ -322,7 +355,7 @@ export class Percy {
322
355
  }
323
356
 
324
357
  // gather snapshots and discover snapshot resources
325
- yield* discoverSnapshotResources(this.#discovery, {
358
+ yield* discoverSnapshotResources(_classPrivateFieldGet2(this, _discovery), {
326
359
  skipDiscovery: this.skipDiscovery,
327
360
  dryRun: this.dryRun,
328
361
  snapshots: yield* gatherSnapshots(options, {
@@ -342,7 +375,7 @@ export class Percy {
342
375
  });
343
376
  }
344
377
  // push each finished snapshot to the snapshots queue
345
- this.#snapshots.push(snapshot);
378
+ _classPrivateFieldGet2(this, _snapshots).push(snapshot);
346
379
  });
347
380
  } finally {
348
381
  var _server;
@@ -402,9 +435,11 @@ export class Percy {
402
435
  // return an async generator to allow cancelation
403
436
  return async function* () {
404
437
  try {
405
- return yield* yieldTo(this.#snapshots.push(options));
438
+ return yield* yieldTo(_classPrivateFieldGet2(this, _snapshots).push(options));
406
439
  } catch (error) {
407
- this.#snapshots.cancel(options);
440
+ _classPrivateFieldGet2(this, _snapshots).cancel(options);
441
+ // Detecting and suggesting fix for errors;
442
+ await this.suggestionsForFix(error.message);
408
443
  throw error;
409
444
  }
410
445
  }.call(this);
@@ -436,6 +471,48 @@ export class Percy {
436
471
  if (syncMode) options.sync = syncMode;
437
472
  return syncMode;
438
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
+ }
439
516
  async sendBuildLogs() {
440
517
  if (!process.env.PERCY_TOKEN) return;
441
518
  try {
@@ -443,6 +520,7 @@ export class Percy {
443
520
  const logsObject = {
444
521
  clilogs: logger.query(log => !['ci'].includes(log.debug))
445
522
  };
523
+
446
524
  // Only add CI logs if not disabled voluntarily.
447
525
  const sendCILogs = process.env.PERCY_CLIENT_ERROR_LOGS !== 'false';
448
526
  if (sendCILogs) {
@@ -466,4 +544,32 @@ export class Percy {
466
544
  }
467
545
  }
468
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
+ }
469
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-beta.6",
3
+ "version": "1.28.9-beta.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,11 +43,11 @@
43
43
  "test:types": "tsd"
44
44
  },
45
45
  "dependencies": {
46
- "@percy/client": "1.28.8-beta.6",
47
- "@percy/config": "1.28.8-beta.6",
48
- "@percy/dom": "1.28.8-beta.6",
49
- "@percy/logger": "1.28.8-beta.6",
50
- "@percy/webdriver-utils": "1.28.8-beta.6",
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": "33ebd1aded619784a02e27cb26dbd344c7131ee9"
63
+ "gitHead": "a1114f1e18518012f48756c9558a8e7895d2b3a9"
64
64
  }