@pendo/agent 2.293.0 → 2.294.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/debugger-plugin.min.js +1 -1
- package/dist/dom.esm.js +1 -1
- package/dist/pendo.debugger.min.js +8 -8
- package/dist/pendo.module.js +433 -41
- package/dist/pendo.module.min.js +10 -8
- package/package.json +1 -1
package/dist/pendo.module.js
CHANGED
|
@@ -438,7 +438,7 @@ function loadAsModule(config) {
|
|
|
438
438
|
}
|
|
439
439
|
function createPendoObject(config) {
|
|
440
440
|
var windowOrMountPoint = loadAsModule(config) ? {} : window;
|
|
441
|
-
const agentScriptTag = findAgentScriptTag(document.getElementsByTagName('script'), config.apiKey);
|
|
441
|
+
const agentScriptTag = document.currentScript || findAgentScriptTag(document.getElementsByTagName('script'), config.apiKey);
|
|
442
442
|
let globalKey = config.pendoGlobalKey || 'pendo';
|
|
443
443
|
if (agentScriptTag) {
|
|
444
444
|
globalKey = agentScriptTag.getAttribute('data-pendo-global-key') || globalKey;
|
|
@@ -463,9 +463,6 @@ function shouldUseUnminifiedAgent(config, debuggingEnabled) {
|
|
|
463
463
|
return !isExtension(config) && isMinifiedAgent(config) && debuggingEnabled;
|
|
464
464
|
}
|
|
465
465
|
function findAgentScriptTag(scripts = [], apiKey) {
|
|
466
|
-
if (document.currentScript) {
|
|
467
|
-
return document.currentScript;
|
|
468
|
-
}
|
|
469
466
|
const regex = /^https:\/\/[\w\-.]*cdn[\w\-.]*\.(pendo-dev\.com|pendo\.io)\/agent\/static\/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}|PENDO_API_KEY)\/pendo\.js$/g;
|
|
470
467
|
for (let i = 0; i < scripts.length; i++) {
|
|
471
468
|
const script = scripts[i];
|
|
@@ -3911,8 +3908,8 @@ var SERVER = '';
|
|
|
3911
3908
|
var ASSET_HOST = '';
|
|
3912
3909
|
var ASSET_PATH = '';
|
|
3913
3910
|
var DESIGNER_SERVER = '';
|
|
3914
|
-
var VERSION = '2.
|
|
3915
|
-
var PACKAGE_VERSION = '2.
|
|
3911
|
+
var VERSION = '2.294.0_';
|
|
3912
|
+
var PACKAGE_VERSION = '2.294.0';
|
|
3916
3913
|
var LOADER = 'xhr';
|
|
3917
3914
|
/* eslint-enable agent-eslint-rules/no-gulp-env-references */
|
|
3918
3915
|
/**
|
|
@@ -12234,6 +12231,7 @@ class PerformanceMonitor {
|
|
|
12234
12231
|
return detectNativeBrowserAPI('performance.mark') &&
|
|
12235
12232
|
detectNativeBrowserAPI('performance.measure') &&
|
|
12236
12233
|
detectNativeBrowserAPI('performance.getEntries') &&
|
|
12234
|
+
detectNativeBrowserAPI('performance.getEntriesByName') &&
|
|
12237
12235
|
detectNativeBrowserAPI('performance.clearMarks') &&
|
|
12238
12236
|
detectNativeBrowserAPI('performance.clearMeasures');
|
|
12239
12237
|
}
|
|
@@ -12253,8 +12251,10 @@ class PerformanceMonitor {
|
|
|
12253
12251
|
name = `pendo-${name}`;
|
|
12254
12252
|
const startMark = `${name}-start`;
|
|
12255
12253
|
const stopMark = `${name}-stop`;
|
|
12256
|
-
performance.
|
|
12257
|
-
|
|
12254
|
+
if (performance.getEntriesByName(startMark).length && performance.getEntriesByName(stopMark).length) {
|
|
12255
|
+
performance.measure(name, startMark, stopMark);
|
|
12256
|
+
this._count(this._measures, name);
|
|
12257
|
+
}
|
|
12258
12258
|
}
|
|
12259
12259
|
_count(registry, name) {
|
|
12260
12260
|
if (!registry[name]) {
|
|
@@ -13307,6 +13307,7 @@ var getValidTarget = function (node) {
|
|
|
13307
13307
|
*/
|
|
13308
13308
|
var handle_event = function (evt) {
|
|
13309
13309
|
try {
|
|
13310
|
+
PerformanceMonitor$1.startTimer('event-captured');
|
|
13310
13311
|
if (dom.data.get(evt, 'counted'))
|
|
13311
13312
|
return;
|
|
13312
13313
|
dom.data.set(evt, 'counted', true);
|
|
@@ -13346,6 +13347,9 @@ var handle_event = function (evt) {
|
|
|
13346
13347
|
catch (e) {
|
|
13347
13348
|
log.critical('pendo.io while handling event', { error: e });
|
|
13348
13349
|
}
|
|
13350
|
+
finally {
|
|
13351
|
+
PerformanceMonitor$1.stopTimer('event-captured');
|
|
13352
|
+
}
|
|
13349
13353
|
};
|
|
13350
13354
|
function getClickEventProperties(target) {
|
|
13351
13355
|
const eventPropertyHandler = getEventPropertyHandler(target);
|
|
@@ -17869,6 +17873,9 @@ function getAllowedAttributes(attributeKeyValueMap, stepId, guideId, type) {
|
|
|
17869
17873
|
function buildNodeFromJSON(json, step, guides) {
|
|
17870
17874
|
step = step || { id: 'unknown', guideId: 'unknown' };
|
|
17871
17875
|
json.props = getAllowedAttributes(json.props, step.id, step.guideId, json.type);
|
|
17876
|
+
if (step.isDarkMode && json.darkModeProps) {
|
|
17877
|
+
json.darkModeProps = getAllowedAttributes(json.darkModeProps, step.id, step.guideId, json.type);
|
|
17878
|
+
}
|
|
17872
17879
|
var curNode = dom(document.createElement(json.type));
|
|
17873
17880
|
// APP-81040 calling code in the pendo app (and possibly elsewhere) depends on
|
|
17874
17881
|
// curNode.getParent() returning non-null in some cases. The fact that it used to
|
|
@@ -17888,7 +17895,11 @@ function buildNodeFromJSON(json, step, guides) {
|
|
|
17888
17895
|
}
|
|
17889
17896
|
_.each(json.props, function (propValue, propKey) {
|
|
17890
17897
|
if (propKey === 'style') {
|
|
17891
|
-
|
|
17898
|
+
let nodeStyle = json.props.style;
|
|
17899
|
+
if (step.isDarkMode && json.darkModeProps) {
|
|
17900
|
+
nodeStyle = Object.assign(Object.assign({}, json.props.style), json.darkModeProps.style);
|
|
17901
|
+
}
|
|
17902
|
+
curNode.css(nodeStyle);
|
|
17892
17903
|
}
|
|
17893
17904
|
else if (propKey === 'data-pendo-code-block' && propValue === true && !ConfigReader.get('preventCodeInjection')) {
|
|
17894
17905
|
const htmlString = step.getContent();
|
|
@@ -17913,13 +17924,23 @@ function buildNodeFromJSON(json, step, guides) {
|
|
|
17913
17924
|
if (nonce) {
|
|
17914
17925
|
curNode.attr('nonce', nonce);
|
|
17915
17926
|
}
|
|
17927
|
+
let css = json.css;
|
|
17928
|
+
if (json.forDarkMode) {
|
|
17929
|
+
const darkModeSelector = _.get(step, 'attributes.darkMode.selector', '');
|
|
17930
|
+
css = _.map(css, function (rule) {
|
|
17931
|
+
return {
|
|
17932
|
+
styles: rule.styles,
|
|
17933
|
+
selector: `${darkModeSelector} ${rule.selector}`
|
|
17934
|
+
};
|
|
17935
|
+
});
|
|
17936
|
+
}
|
|
17916
17937
|
// TODO: make this render building-block pseudo-styles properly for IE7-8. This current functionality allows guides to render in IE but there are lots of styling problems.
|
|
17917
17938
|
// `curNode.text` specifically breaks in IE8 since style tags text attributes are read only. From researching `node.styleSheet.cssText` is the correct way to do it.
|
|
17918
17939
|
if (curNode.styleSheet) {
|
|
17919
|
-
curNode.styleSheet.cssText = buildStyleTagContent(
|
|
17940
|
+
curNode.styleSheet.cssText = buildStyleTagContent(css);
|
|
17920
17941
|
}
|
|
17921
17942
|
else {
|
|
17922
|
-
curNode.text(buildStyleTagContent(
|
|
17943
|
+
curNode.text(buildStyleTagContent(css));
|
|
17923
17944
|
}
|
|
17924
17945
|
}
|
|
17925
17946
|
if (json.svgWidgetId) {
|
|
@@ -20617,6 +20638,21 @@ class AutoDisplayPhase {
|
|
|
20617
20638
|
}
|
|
20618
20639
|
}
|
|
20619
20640
|
|
|
20641
|
+
function syncColorMode(step, rerender = false) {
|
|
20642
|
+
const darkModeSelector = _.get(step, 'attributes.darkMode.selector', '');
|
|
20643
|
+
if (!darkModeSelector)
|
|
20644
|
+
return;
|
|
20645
|
+
const isDarkMode = pendo$1.Sizzle(darkModeSelector).length > 0;
|
|
20646
|
+
const darkModeChanged = step.isDarkMode !== isDarkMode;
|
|
20647
|
+
if (darkModeChanged) {
|
|
20648
|
+
step.isDarkMode = isDarkMode;
|
|
20649
|
+
if (rerender) {
|
|
20650
|
+
step.hide();
|
|
20651
|
+
step.show(step.seenReason);
|
|
20652
|
+
}
|
|
20653
|
+
}
|
|
20654
|
+
}
|
|
20655
|
+
|
|
20620
20656
|
/*
|
|
20621
20657
|
* Guide Loop
|
|
20622
20658
|
*
|
|
@@ -20815,6 +20851,7 @@ function stepShowingProc(guide, step) {
|
|
|
20815
20851
|
step.attributes.currentTextZoomFontSize = currentBrowserFontSize;
|
|
20816
20852
|
}
|
|
20817
20853
|
}
|
|
20854
|
+
syncColorMode(step, true);
|
|
20818
20855
|
if (step.elementPathRule && targetElement && !SizzleProxy.matchesSelector(targetElement, step.elementPathRule)) {
|
|
20819
20856
|
step.hide();
|
|
20820
20857
|
return;
|
|
@@ -26343,53 +26380,152 @@ class NetworkRequestIntercept {
|
|
|
26343
26380
|
extractXHRHeaders(xhr) {
|
|
26344
26381
|
return xhr[PENDO_HEADERS_KEY] || {};
|
|
26345
26382
|
}
|
|
26346
|
-
|
|
26383
|
+
generateRequestId() {
|
|
26384
|
+
const $stringLength = 8;
|
|
26385
|
+
return 'req_' + Date.now() + '_' + pendo$1.randomString($stringLength);
|
|
26386
|
+
}
|
|
26387
|
+
safelyReadResponse(response) {
|
|
26388
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26389
|
+
try {
|
|
26390
|
+
const contentType = response.headers.get('content-type') || '';
|
|
26391
|
+
if (contentType.indexOf('application/json') !== -1) {
|
|
26392
|
+
return yield response.json();
|
|
26393
|
+
}
|
|
26394
|
+
else if (contentType.indexOf('text/') !== -1) {
|
|
26395
|
+
return yield response.text();
|
|
26396
|
+
}
|
|
26397
|
+
else {
|
|
26398
|
+
// For binary or unknown content types, just capture metadata
|
|
26399
|
+
return `[Binary content: ${contentType}]`;
|
|
26400
|
+
}
|
|
26401
|
+
}
|
|
26402
|
+
catch (e) {
|
|
26403
|
+
return '[Unable to read response body]';
|
|
26404
|
+
}
|
|
26405
|
+
});
|
|
26406
|
+
}
|
|
26407
|
+
extractResponseHeaders(response) {
|
|
26408
|
+
let headers = {};
|
|
26409
|
+
if (response.headers) {
|
|
26410
|
+
const headerEntries = Array.from(response.headers.entries());
|
|
26411
|
+
headers = this.entriesToObject(headerEntries);
|
|
26412
|
+
}
|
|
26413
|
+
return headers;
|
|
26414
|
+
}
|
|
26415
|
+
parseXHRResponseHeaders(headerString) {
|
|
26416
|
+
const headers = {};
|
|
26417
|
+
if (!headerString)
|
|
26418
|
+
return headers;
|
|
26419
|
+
const lines = headerString.split('\r\n');
|
|
26420
|
+
_.each(lines, line => {
|
|
26421
|
+
const parts = line.split(': ');
|
|
26422
|
+
if (parts.length === 2) {
|
|
26423
|
+
headers[parts[0]] = parts[1];
|
|
26424
|
+
}
|
|
26425
|
+
});
|
|
26426
|
+
return headers;
|
|
26427
|
+
}
|
|
26428
|
+
patchFetch() {
|
|
26347
26429
|
const networkInterceptor = this;
|
|
26348
|
-
if (networkInterceptor._networkPatched)
|
|
26349
|
-
return;
|
|
26350
|
-
networkInterceptor._networkPatched = true;
|
|
26351
|
-
networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
26352
|
-
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
|
|
26353
|
-
this[PENDO_HEADERS_KEY] = this[PENDO_HEADERS_KEY] || {};
|
|
26354
|
-
this[PENDO_HEADERS_KEY][header] = value;
|
|
26355
|
-
return networkInterceptor._originalSetRequestHeader.apply(this, arguments);
|
|
26356
|
-
};
|
|
26357
26430
|
networkInterceptor._originalFetch = window.fetch;
|
|
26358
26431
|
window.fetch = function (...args) {
|
|
26359
26432
|
return __awaiter(this, void 0, void 0, function* () {
|
|
26360
26433
|
const [url, config = {}] = args;
|
|
26361
26434
|
const method = config.method || 'GET';
|
|
26435
|
+
const requestId = networkInterceptor.generateRequestId();
|
|
26436
|
+
// Capture request data
|
|
26362
26437
|
try {
|
|
26363
26438
|
const headers = networkInterceptor.extractHeaders(config.headers);
|
|
26364
26439
|
const body = networkInterceptor.safelySerializeBody(config.body);
|
|
26365
26440
|
_.each(networkInterceptor.callbackFns, ({ request }) => {
|
|
26366
26441
|
if (_.isFunction(request))
|
|
26367
|
-
request({ method, url, body, headers });
|
|
26442
|
+
request({ requestId, method, url, body, headers });
|
|
26368
26443
|
});
|
|
26369
26444
|
}
|
|
26370
26445
|
catch (e) {
|
|
26371
26446
|
_.each(networkInterceptor.callbackFns, ({ error }) => {
|
|
26372
26447
|
if (_.isFunction(error))
|
|
26373
|
-
error({ context: '
|
|
26448
|
+
error({ context: 'fetchRequest', error: e });
|
|
26449
|
+
});
|
|
26450
|
+
}
|
|
26451
|
+
// Make the actual fetch call and capture response
|
|
26452
|
+
try {
|
|
26453
|
+
const res = yield networkInterceptor._originalFetch.apply(this, args);
|
|
26454
|
+
// Clone the response to avoid consuming the body
|
|
26455
|
+
const responseClone = res.clone();
|
|
26456
|
+
try {
|
|
26457
|
+
// Capture response data
|
|
26458
|
+
const responseBody = yield networkInterceptor.safelyReadResponse(responseClone);
|
|
26459
|
+
const { status, statusText } = res;
|
|
26460
|
+
const headers = networkInterceptor.extractResponseHeaders(res);
|
|
26461
|
+
_.each(networkInterceptor.callbackFns, ({ response }) => {
|
|
26462
|
+
if (_.isFunction(response)) {
|
|
26463
|
+
response({
|
|
26464
|
+
requestId,
|
|
26465
|
+
status,
|
|
26466
|
+
statusText,
|
|
26467
|
+
body: responseBody,
|
|
26468
|
+
headers,
|
|
26469
|
+
url,
|
|
26470
|
+
method
|
|
26471
|
+
});
|
|
26472
|
+
}
|
|
26473
|
+
});
|
|
26474
|
+
}
|
|
26475
|
+
catch (e) {
|
|
26476
|
+
_.each(networkInterceptor.callbackFns, ({ error }) => {
|
|
26477
|
+
if (_.isFunction(error))
|
|
26478
|
+
error({ context: 'fetchResponse', error: e });
|
|
26479
|
+
});
|
|
26480
|
+
}
|
|
26481
|
+
return res;
|
|
26482
|
+
}
|
|
26483
|
+
catch (error) {
|
|
26484
|
+
// Handle network errors
|
|
26485
|
+
_.each(networkInterceptor.callbackFns, ({ response }) => {
|
|
26486
|
+
if (_.isFunction(response)) {
|
|
26487
|
+
response({
|
|
26488
|
+
requestId,
|
|
26489
|
+
status: 0,
|
|
26490
|
+
statusText: 'Network Error',
|
|
26491
|
+
headers: {},
|
|
26492
|
+
body: null,
|
|
26493
|
+
url,
|
|
26494
|
+
method,
|
|
26495
|
+
error: error.message
|
|
26496
|
+
});
|
|
26497
|
+
}
|
|
26374
26498
|
});
|
|
26499
|
+
throw error;
|
|
26375
26500
|
}
|
|
26376
|
-
return networkInterceptor._originalFetch.apply(this, args);
|
|
26377
26501
|
});
|
|
26378
26502
|
};
|
|
26503
|
+
}
|
|
26504
|
+
patchXHR() {
|
|
26505
|
+
const networkInterceptor = this;
|
|
26506
|
+
networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
26379
26507
|
networkInterceptor._originalXHROpen = XMLHttpRequest.prototype.open;
|
|
26380
26508
|
networkInterceptor._originalXHRSend = XMLHttpRequest.prototype.send;
|
|
26509
|
+
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
|
|
26510
|
+
this[PENDO_HEADERS_KEY] = this[PENDO_HEADERS_KEY] || {};
|
|
26511
|
+
this[PENDO_HEADERS_KEY][header] = value;
|
|
26512
|
+
return networkInterceptor._originalSetRequestHeader.apply(this, arguments);
|
|
26513
|
+
};
|
|
26381
26514
|
XMLHttpRequest.prototype.open = function (method, url) {
|
|
26382
|
-
this._method = method;
|
|
26515
|
+
this._method = method;
|
|
26383
26516
|
this._url = url;
|
|
26517
|
+
this._requestId = networkInterceptor.generateRequestId();
|
|
26384
26518
|
return networkInterceptor._originalXHROpen.apply(this, arguments);
|
|
26385
26519
|
};
|
|
26386
26520
|
XMLHttpRequest.prototype.send = function (body) {
|
|
26521
|
+
const xhr = this;
|
|
26522
|
+
const headers = networkInterceptor.extractXHRHeaders(this);
|
|
26523
|
+
const safeBody = networkInterceptor.safelySerializeBody(body);
|
|
26387
26524
|
try {
|
|
26388
|
-
const headers = networkInterceptor.extractXHRHeaders(this);
|
|
26389
|
-
const safeBody = networkInterceptor.safelySerializeBody(body);
|
|
26390
26525
|
_.each(networkInterceptor.callbackFns, ({ request }) => {
|
|
26391
26526
|
if (_.isFunction(request)) {
|
|
26392
26527
|
request({
|
|
26528
|
+
requestId: this._requestId,
|
|
26393
26529
|
method: this._method || 'GET',
|
|
26394
26530
|
url: this._url,
|
|
26395
26531
|
body: safeBody,
|
|
@@ -26401,7 +26537,7 @@ class NetworkRequestIntercept {
|
|
|
26401
26537
|
catch (e) {
|
|
26402
26538
|
_.each(networkInterceptor.callbackFns, ({ error }) => {
|
|
26403
26539
|
if (_.isFunction(error))
|
|
26404
|
-
error({ context: '
|
|
26540
|
+
error({ context: 'xhrRequest', error: e });
|
|
26405
26541
|
});
|
|
26406
26542
|
}
|
|
26407
26543
|
// Clean up custom headers after the request completes
|
|
@@ -26410,9 +26546,49 @@ class NetworkRequestIntercept {
|
|
|
26410
26546
|
delete this[PENDO_HEADERS_KEY];
|
|
26411
26547
|
}, { once: true });
|
|
26412
26548
|
}
|
|
26549
|
+
// Set up response tracking
|
|
26550
|
+
const originalOnReadyStateChange = xhr.onreadystatechange;
|
|
26551
|
+
xhr.onreadystatechange = function () {
|
|
26552
|
+
if (xhr.readyState === 4) { // Request completed
|
|
26553
|
+
try {
|
|
26554
|
+
const headers = networkInterceptor.parseXHRResponseHeaders(xhr.getAllResponseHeaders());
|
|
26555
|
+
const { status, statusText, responseText: body, _url } = xhr;
|
|
26556
|
+
_.each(networkInterceptor.callbackFns, ({ response }) => {
|
|
26557
|
+
if (_.isFunction(response)) {
|
|
26558
|
+
response({
|
|
26559
|
+
requestId: xhr._requestId,
|
|
26560
|
+
status,
|
|
26561
|
+
statusText,
|
|
26562
|
+
headers,
|
|
26563
|
+
body,
|
|
26564
|
+
url: _url,
|
|
26565
|
+
method: xhr._method || 'GET'
|
|
26566
|
+
});
|
|
26567
|
+
}
|
|
26568
|
+
});
|
|
26569
|
+
}
|
|
26570
|
+
catch (e) {
|
|
26571
|
+
_.each(networkInterceptor.callbackFns, ({ error }) => {
|
|
26572
|
+
if (_.isFunction(error))
|
|
26573
|
+
error({ context: 'xhrResponse', error: e });
|
|
26574
|
+
});
|
|
26575
|
+
}
|
|
26576
|
+
}
|
|
26577
|
+
if (originalOnReadyStateChange) {
|
|
26578
|
+
originalOnReadyStateChange.apply(this, arguments);
|
|
26579
|
+
}
|
|
26580
|
+
};
|
|
26413
26581
|
return networkInterceptor._originalXHRSend.apply(this, arguments);
|
|
26414
26582
|
};
|
|
26415
26583
|
}
|
|
26584
|
+
patchNetwork() {
|
|
26585
|
+
const networkInterceptor = this;
|
|
26586
|
+
if (networkInterceptor._networkPatched)
|
|
26587
|
+
return;
|
|
26588
|
+
networkInterceptor._networkPatched = true;
|
|
26589
|
+
networkInterceptor.patchFetch();
|
|
26590
|
+
networkInterceptor.patchXHR();
|
|
26591
|
+
}
|
|
26416
26592
|
on({ request, response, error } = {}) {
|
|
26417
26593
|
if (!this.callbackFns) {
|
|
26418
26594
|
this.callbackFns = [];
|
|
@@ -26420,7 +26596,7 @@ class NetworkRequestIntercept {
|
|
|
26420
26596
|
if (_.isFunction(request) || _.isFunction(response) || _.isFunction(error)) {
|
|
26421
26597
|
this.callbackFns.push({ request, response, error });
|
|
26422
26598
|
}
|
|
26423
|
-
if (!this._networkPatched
|
|
26599
|
+
if (!this._networkPatched) {
|
|
26424
26600
|
this.patchNetwork();
|
|
26425
26601
|
}
|
|
26426
26602
|
return this;
|
|
@@ -27240,6 +27416,8 @@ const PluginAPI = {
|
|
|
27240
27416
|
collectEvent
|
|
27241
27417
|
},
|
|
27242
27418
|
attachEvent,
|
|
27419
|
+
attachEventInternal,
|
|
27420
|
+
detachEventInternal,
|
|
27243
27421
|
ConfigReader,
|
|
27244
27422
|
Events,
|
|
27245
27423
|
EventTracer,
|
|
@@ -28204,6 +28382,9 @@ var BuildingBlockGuides = (function () {
|
|
|
28204
28382
|
function renderGuideFromJSON(json, step, guides, options) {
|
|
28205
28383
|
options = options || {};
|
|
28206
28384
|
var guide = step.getGuide();
|
|
28385
|
+
if (step.isDarkMode === undefined) {
|
|
28386
|
+
syncColorMode(step);
|
|
28387
|
+
}
|
|
28207
28388
|
var containerJSON = findGuideContainerJSON(json);
|
|
28208
28389
|
var isResourceCenter = _.get(guide, 'attributes.resourceCenter');
|
|
28209
28390
|
if (isResourceCenter) {
|
|
@@ -31071,6 +31252,10 @@ var GuideDisplay = (function () {
|
|
|
31071
31252
|
if (step.type !== 'whatsnew' && !step.isShown()) {
|
|
31072
31253
|
return false;
|
|
31073
31254
|
}
|
|
31255
|
+
if (isGuideRequestPending()) {
|
|
31256
|
+
log.info('guides are loading.', { contexts: ['guides', 'loading'] });
|
|
31257
|
+
return false;
|
|
31258
|
+
}
|
|
31074
31259
|
step.unlock();
|
|
31075
31260
|
step._show(reason);
|
|
31076
31261
|
return step.isShown();
|
|
@@ -31555,6 +31740,7 @@ function GuideStep(guide) {
|
|
|
31555
31740
|
step.seenState = 'active';
|
|
31556
31741
|
setSeenTime(getNow());
|
|
31557
31742
|
var seenProps = {
|
|
31743
|
+
color_mode: step.isDarkMode ? 'dark' : 'default',
|
|
31558
31744
|
last_updated_at: step.lastUpdatedAt
|
|
31559
31745
|
};
|
|
31560
31746
|
var pollTypes = this.getStepPollTypes(guide, step);
|
|
@@ -33246,7 +33432,7 @@ const DebuggerModule = (() => {
|
|
|
33246
33432
|
location: { type: SYNC_TYPES.BOTTOM_UP, defaultValue: {} }
|
|
33247
33433
|
};
|
|
33248
33434
|
const actions = {
|
|
33249
|
-
init: (context) => {
|
|
33435
|
+
init: (context, buffer) => {
|
|
33250
33436
|
context.commit('setFrameId', EventTracer.getFrameId());
|
|
33251
33437
|
context.commit('setTabId', EventTracer.getTabId());
|
|
33252
33438
|
context.commit('setInstallType', getInstallType());
|
|
@@ -33266,6 +33452,7 @@ const DebuggerModule = (() => {
|
|
|
33266
33452
|
frameId: context.state.frameId
|
|
33267
33453
|
});
|
|
33268
33454
|
}
|
|
33455
|
+
_.each(buffer.events, (event) => eventCapturedFn(event));
|
|
33269
33456
|
},
|
|
33270
33457
|
join: (context, data) => {
|
|
33271
33458
|
if (context.state.frameId === data.frameId)
|
|
@@ -36959,12 +37146,18 @@ function disableDebugging() {
|
|
|
36959
37146
|
_.extend(debug, debugging);
|
|
36960
37147
|
|
|
36961
37148
|
const waitForLeader = (Events, store, q) => {
|
|
37149
|
+
const buffer = {
|
|
37150
|
+
events: []
|
|
37151
|
+
};
|
|
37152
|
+
Events.on('eventCaptured', (evt) => {
|
|
37153
|
+
buffer.events.push(evt);
|
|
37154
|
+
});
|
|
36962
37155
|
if (store.getters['frames/leaderExists']()) {
|
|
36963
|
-
return q.resolve();
|
|
37156
|
+
return q.resolve(buffer);
|
|
36964
37157
|
}
|
|
36965
37158
|
const deferred = q.defer();
|
|
36966
37159
|
Events.on('leaderChanged', () => {
|
|
36967
|
-
deferred.resolve();
|
|
37160
|
+
deferred.resolve(buffer);
|
|
36968
37161
|
});
|
|
36969
37162
|
return deferred.promise;
|
|
36970
37163
|
};
|
|
@@ -36979,8 +37172,8 @@ const DebuggerLauncher = (function () {
|
|
|
36979
37172
|
function initialize(pendo, PluginAPI) {
|
|
36980
37173
|
const { Events, q } = PluginAPI;
|
|
36981
37174
|
store = PluginAPI.store;
|
|
36982
|
-
waitForLeader(Events, store, q).then(() => {
|
|
36983
|
-
store.dispatch('debugger/init');
|
|
37175
|
+
waitForLeader(Events, store, q).then((buffer) => {
|
|
37176
|
+
store.dispatch('debugger/init', buffer);
|
|
36984
37177
|
if (store.getters['frames/isLeader']()) {
|
|
36985
37178
|
startDebuggingModuleIfEnabled();
|
|
36986
37179
|
}
|
|
@@ -44346,13 +44539,15 @@ class MutationBuffer {
|
|
|
44346
44539
|
index.childNodes(textarea),
|
|
44347
44540
|
(cn) => index.textContent(cn) || ""
|
|
44348
44541
|
).join("");
|
|
44542
|
+
const needsMask = needMaskingText(textarea, this.maskTextClass, this.maskTextSelector, true);
|
|
44349
44543
|
item.attributes.value = maskInputValue({
|
|
44350
44544
|
element: textarea,
|
|
44351
44545
|
maskInputOptions: this.maskInputOptions,
|
|
44352
44546
|
tagName: textarea.tagName,
|
|
44353
44547
|
type: getInputType(textarea),
|
|
44354
44548
|
value,
|
|
44355
|
-
maskInputFn: this.maskInputFn
|
|
44549
|
+
maskInputFn: this.maskInputFn,
|
|
44550
|
+
needsMask
|
|
44356
44551
|
});
|
|
44357
44552
|
});
|
|
44358
44553
|
__publicField(this, "processMutation", (m) => {
|
|
@@ -52558,6 +52753,12 @@ class SessionRecorder {
|
|
|
52558
52753
|
return;
|
|
52559
52754
|
clearTimeout(this._changeIdentityTimer);
|
|
52560
52755
|
if (this.isRecording()) {
|
|
52756
|
+
this.logStopReason('IDENTITY_CHANGED', {
|
|
52757
|
+
previousVisitorId: this.visitorId,
|
|
52758
|
+
newVisitorId: identifyEvent.data[0].visitor_id,
|
|
52759
|
+
previousAccountId: this.accountId,
|
|
52760
|
+
newAccountId: identifyEvent.data[0].account_id
|
|
52761
|
+
});
|
|
52561
52762
|
this.stop();
|
|
52562
52763
|
}
|
|
52563
52764
|
this.visitorId = identifyEvent.data[0].visitor_id;
|
|
@@ -52653,7 +52854,7 @@ class SessionRecorder {
|
|
|
52653
52854
|
this._start();
|
|
52654
52855
|
}
|
|
52655
52856
|
_markEvents(events) {
|
|
52656
|
-
if (!this.recordingId)
|
|
52857
|
+
if (!this.recordingId || !this.isRecording())
|
|
52657
52858
|
return;
|
|
52658
52859
|
for (var e of events) {
|
|
52659
52860
|
if ((e.visitor_id === this.visitorId || e.visitorId === this.visitorId) && e.type !== 'identify') {
|
|
@@ -52971,6 +53172,8 @@ class SessionRecorder {
|
|
|
52971
53172
|
}
|
|
52972
53173
|
}
|
|
52973
53174
|
addRecordingId(event) {
|
|
53175
|
+
if (!this.isRecording())
|
|
53176
|
+
return;
|
|
52974
53177
|
if (!this.recordingId || !event || !event.data || !event.data.length)
|
|
52975
53178
|
return;
|
|
52976
53179
|
var capturedEvent = event.data[0];
|
|
@@ -52979,6 +53182,10 @@ class SessionRecorder {
|
|
|
52979
53182
|
if (capturedEvent.visitor_id !== this.visitorId && capturedEvent.visitorId !== this.visitorId) {
|
|
52980
53183
|
// visitor id has already diverged from the agent, we'll stop sending events and just return here
|
|
52981
53184
|
this.api.log.warn('Visitor id has diverged from agent');
|
|
53185
|
+
this.logStopReason('VISITOR_ID_DIVERGED', {
|
|
53186
|
+
agentVisitorId: capturedEvent.visitor_id || capturedEvent.visitorId,
|
|
53187
|
+
recordingVisitorId: this.visitorId
|
|
53188
|
+
});
|
|
52982
53189
|
this.stop();
|
|
52983
53190
|
return;
|
|
52984
53191
|
}
|
|
@@ -54158,6 +54365,168 @@ function scrubPII(string) {
|
|
|
54158
54365
|
return Object.values(PII_PATTERN).reduce((str, pattern) => str.replace(pattern, PII_REPLACEMENT), string);
|
|
54159
54366
|
}
|
|
54160
54367
|
|
|
54368
|
+
/**
|
|
54369
|
+
* Determines the type of resource that was blocked based on the blocked URI and CSP directive.
|
|
54370
|
+
*
|
|
54371
|
+
* @param {string} blockedURI - The URI that was blocked by the CSP policy (can be 'inline', 'eval', or a URL)
|
|
54372
|
+
* @param {string} directive - The CSP directive that caused the violation (e.g., 'script-src', 'style-src')
|
|
54373
|
+
* @returns {string} A human-readable description of the resource type
|
|
54374
|
+
*
|
|
54375
|
+
* @example
|
|
54376
|
+
* getResourceType('inline', 'script-src') // returns 'inline script'
|
|
54377
|
+
* getResourceType('https://example.com/script.js', 'script-src') // returns 'script'
|
|
54378
|
+
* getResourceType('https://example.com/image.jpg', 'img-src') // returns 'image'
|
|
54379
|
+
* getResourceType('https://example.com/worker.js', 'worker-src') // returns 'worker'
|
|
54380
|
+
*/
|
|
54381
|
+
function getResourceType(blockedURI, directive) {
|
|
54382
|
+
if (!directive || typeof directive !== 'string')
|
|
54383
|
+
return 'resource';
|
|
54384
|
+
const d = directive.toLowerCase();
|
|
54385
|
+
if (blockedURI === 'inline') {
|
|
54386
|
+
if (d.includes('script-src-attr')) {
|
|
54387
|
+
return 'inline event handler';
|
|
54388
|
+
}
|
|
54389
|
+
if (d.includes('style-src-attr')) {
|
|
54390
|
+
return 'inline style attribute';
|
|
54391
|
+
}
|
|
54392
|
+
if (d.includes('script')) {
|
|
54393
|
+
return 'inline script';
|
|
54394
|
+
}
|
|
54395
|
+
if (d.includes('style')) {
|
|
54396
|
+
return 'inline style';
|
|
54397
|
+
}
|
|
54398
|
+
}
|
|
54399
|
+
if (blockedURI === 'eval') {
|
|
54400
|
+
return 'eval script execution';
|
|
54401
|
+
}
|
|
54402
|
+
if (d.includes('worker'))
|
|
54403
|
+
return 'worker';
|
|
54404
|
+
if (d.includes('script')) {
|
|
54405
|
+
return d.includes('elem') ? 'script element' : 'script';
|
|
54406
|
+
}
|
|
54407
|
+
if (d.includes('style')) {
|
|
54408
|
+
return d.includes('elem') ? 'style element' : 'stylesheet';
|
|
54409
|
+
}
|
|
54410
|
+
if (d.includes('img'))
|
|
54411
|
+
return 'image';
|
|
54412
|
+
if (d.includes('font'))
|
|
54413
|
+
return 'font';
|
|
54414
|
+
if (d.includes('connect'))
|
|
54415
|
+
return 'network request';
|
|
54416
|
+
if (d.includes('media'))
|
|
54417
|
+
return 'media resource';
|
|
54418
|
+
if (d.includes('frame-ancestors'))
|
|
54419
|
+
return 'display of your page in a frame';
|
|
54420
|
+
if (d.includes('frame'))
|
|
54421
|
+
return 'frame';
|
|
54422
|
+
if (d.includes('manifest'))
|
|
54423
|
+
return 'manifest';
|
|
54424
|
+
if (d.includes('base-uri'))
|
|
54425
|
+
return 'base URI';
|
|
54426
|
+
if (d.includes('form-action'))
|
|
54427
|
+
return 'form submission';
|
|
54428
|
+
return 'resource';
|
|
54429
|
+
}
|
|
54430
|
+
/**
|
|
54431
|
+
* Finds a specific directive in a CSP policy string and returns it with its value.
|
|
54432
|
+
*
|
|
54433
|
+
* @param {string} policy - The complete original CSP policy string (semicolon-separated directives)
|
|
54434
|
+
* @param {string} directiveName - The name of the directive to find (e.g., 'script-src', 'style-src')
|
|
54435
|
+
* @returns {string} The complete directive with its value if found, empty string otherwise
|
|
54436
|
+
*
|
|
54437
|
+
* @example
|
|
54438
|
+
* getDirective('script-src \'self\'; style-src \'self\'', 'script-src') // returns 'script-src \'self\''
|
|
54439
|
+
*/
|
|
54440
|
+
function getDirective(policy, directiveName) {
|
|
54441
|
+
if (!policy || !directiveName)
|
|
54442
|
+
return '';
|
|
54443
|
+
const needle = directiveName.toLowerCase();
|
|
54444
|
+
for (const directive of policy.split(';')) {
|
|
54445
|
+
const trimmed = directive.trim();
|
|
54446
|
+
const lower = trimmed.toLowerCase();
|
|
54447
|
+
if (lower === needle || lower.startsWith(`${needle} `)) {
|
|
54448
|
+
return trimmed;
|
|
54449
|
+
}
|
|
54450
|
+
}
|
|
54451
|
+
return '';
|
|
54452
|
+
}
|
|
54453
|
+
/**
|
|
54454
|
+
* Determines the appropriate article (A/An) for a given phrase based on its first letter.
|
|
54455
|
+
*
|
|
54456
|
+
* @param {string} phrase - The phrase to determine the article for
|
|
54457
|
+
* @returns {string} Either 'A' or 'An' based on whether the phrase starts with a vowel sound
|
|
54458
|
+
*
|
|
54459
|
+
* @example
|
|
54460
|
+
* getArticle('script') // returns 'A'
|
|
54461
|
+
* getArticle('image') // returns 'An'
|
|
54462
|
+
* getArticle('stylesheet') // returns 'A'
|
|
54463
|
+
* getArticle('') // returns 'A' (fallback for empty string)
|
|
54464
|
+
* getArticle(undefined) // returns 'A' (fallback for undefined)
|
|
54465
|
+
*/
|
|
54466
|
+
function getArticle(phrase) {
|
|
54467
|
+
if (!phrase || typeof phrase !== 'string')
|
|
54468
|
+
return 'A';
|
|
54469
|
+
const c = phrase.trim().charAt(0).toLowerCase();
|
|
54470
|
+
return 'aeiou'.includes(c) ? 'An' : 'A';
|
|
54471
|
+
}
|
|
54472
|
+
/**
|
|
54473
|
+
* Returns the original blocked URI when it looks like a URL or scheme,
|
|
54474
|
+
* otherwise returns an empty string for special cases.
|
|
54475
|
+
*
|
|
54476
|
+
* @param {string} blockedURI - The URI that was blocked (can be 'inline', 'eval', or a URL/scheme)
|
|
54477
|
+
* @returns {string} The blocked URI or an empty string for special cases like 'inline' or 'eval'
|
|
54478
|
+
*
|
|
54479
|
+
* @example
|
|
54480
|
+
* formatBlockedUri('https://example.com/script.js') // returns 'https://example.com/script.js'
|
|
54481
|
+
* formatBlockedUri('inline') // returns ''
|
|
54482
|
+
* formatBlockedUri('blob') // returns 'blob'
|
|
54483
|
+
*/
|
|
54484
|
+
function formatBlockedUri(blockedURI) {
|
|
54485
|
+
if (!blockedURI || blockedURI === 'inline' || blockedURI === 'eval')
|
|
54486
|
+
return '';
|
|
54487
|
+
return blockedURI;
|
|
54488
|
+
}
|
|
54489
|
+
/**
|
|
54490
|
+
* Formats a CSP violation message for console output.
|
|
54491
|
+
*
|
|
54492
|
+
* @param {string} blockedURI - The URI that was blocked by the CSP policy (can be 'inline', 'eval', or a URL)
|
|
54493
|
+
* @param {string} directive - The CSP directive that caused the violation (e.g., 'script-src', 'frame-ancestors')
|
|
54494
|
+
* @param {boolean} isReportOnly - Whether this is a report-only policy violation (adds " (Report-Only)" to message)
|
|
54495
|
+
* @param {string} originalPolicy - The complete CSP policy that was violated
|
|
54496
|
+
* @returns {string} A formatted readable CSP violation message
|
|
54497
|
+
*
|
|
54498
|
+
* @example
|
|
54499
|
+
* createCspViolationMessage(
|
|
54500
|
+
* 'https://example.com/script.js',
|
|
54501
|
+
* 'script-src',
|
|
54502
|
+
* false,
|
|
54503
|
+
* 'script-src \'self\'; style-src \'self\''
|
|
54504
|
+
* )
|
|
54505
|
+
* // returns: "Content Security Policy: A script from https://example.com/script.js was blocked by your site's `script-src 'self'` policy.\nCurrent CSP: \"script-src 'self'; style-src 'self'\"."
|
|
54506
|
+
*/
|
|
54507
|
+
function createCspViolationMessage(blockedURI, directive, isReportOnly, originalPolicy) {
|
|
54508
|
+
if (!directive || typeof directive !== 'string') {
|
|
54509
|
+
return 'Content Security Policy: Unknown violation occurred.';
|
|
54510
|
+
}
|
|
54511
|
+
try {
|
|
54512
|
+
const reportOnlyText = isReportOnly ? ' (Report-Only)' : '';
|
|
54513
|
+
// special case for frame-ancestors since it doesn't fit our template at all
|
|
54514
|
+
if ((directive === null || directive === void 0 ? void 0 : directive.toLowerCase()) === 'frame-ancestors') {
|
|
54515
|
+
return `Content Security Policy${reportOnlyText}: The display of ${blockedURI} in a frame was blocked because an ancestor violates your site's frame-ancestors policy.\nCurrent CSP: "${originalPolicy}".`;
|
|
54516
|
+
}
|
|
54517
|
+
const resourceType = getResourceType(blockedURI, directive);
|
|
54518
|
+
const article = getArticle(resourceType);
|
|
54519
|
+
const source = formatBlockedUri(blockedURI);
|
|
54520
|
+
const directiveValue = getDirective(originalPolicy, directive);
|
|
54521
|
+
const policyDisplay = directiveValue || directive;
|
|
54522
|
+
const resourceDescription = `${article} ${resourceType}${source ? ` from ${source}` : ''}`;
|
|
54523
|
+
return `Content Security Policy${reportOnlyText}: ${resourceDescription} was blocked by your site's \`${policyDisplay}\` policy.\nCurrent CSP: "${originalPolicy}".`;
|
|
54524
|
+
}
|
|
54525
|
+
catch (error) {
|
|
54526
|
+
return `Content Security Policy: Error formatting violation message: ${error.message}`;
|
|
54527
|
+
}
|
|
54528
|
+
}
|
|
54529
|
+
|
|
54161
54530
|
const DEV_LOG_LEVELS = ['info', 'warn', 'error'];
|
|
54162
54531
|
const TOKEN_MAX_SIZE = 100;
|
|
54163
54532
|
const TOKEN_REFILL_RATE = 10;
|
|
@@ -56292,6 +56661,7 @@ function ConsoleCapture() {
|
|
|
56292
56661
|
onAppUnloaded,
|
|
56293
56662
|
onPtmPaused,
|
|
56294
56663
|
onPtmUnpaused,
|
|
56664
|
+
securityPolicyViolationFn,
|
|
56295
56665
|
get buffer() {
|
|
56296
56666
|
return buffer;
|
|
56297
56667
|
},
|
|
@@ -56320,7 +56690,7 @@ function ConsoleCapture() {
|
|
|
56320
56690
|
}
|
|
56321
56691
|
}, SEND_INTERVAL);
|
|
56322
56692
|
sendQueue.start();
|
|
56323
|
-
pluginAPI.Events.ready.on(
|
|
56693
|
+
pluginAPI.Events.ready.on(readyHandler);
|
|
56324
56694
|
pluginAPI.Events.appUnloaded.on(onAppUnloaded);
|
|
56325
56695
|
pluginAPI.Events.appHidden.on(onAppHidden);
|
|
56326
56696
|
pluginAPI.Events['ptm:paused'].on(onPtmPaused);
|
|
@@ -56355,6 +56725,10 @@ function ConsoleCapture() {
|
|
|
56355
56725
|
url: globalPendo.url.get()
|
|
56356
56726
|
};
|
|
56357
56727
|
}
|
|
56728
|
+
function readyHandler() {
|
|
56729
|
+
addIntercepts();
|
|
56730
|
+
pluginAPI.attachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
|
|
56731
|
+
}
|
|
56358
56732
|
function addIntercepts() {
|
|
56359
56733
|
_.each(CONSOLE_METHODS, function (methodName) {
|
|
56360
56734
|
const originalMethod = console[methodName];
|
|
@@ -56371,7 +56745,21 @@ function ConsoleCapture() {
|
|
|
56371
56745
|
};
|
|
56372
56746
|
});
|
|
56373
56747
|
}
|
|
56374
|
-
function
|
|
56748
|
+
function securityPolicyViolationFn(evt) {
|
|
56749
|
+
if (!evt || typeof evt !== 'object')
|
|
56750
|
+
return;
|
|
56751
|
+
const { blockedURI, violatedDirective, effectiveDirective, disposition, originalPolicy } = evt;
|
|
56752
|
+
const directive = violatedDirective || effectiveDirective;
|
|
56753
|
+
const isReportOnly = disposition === 'report';
|
|
56754
|
+
const message = createCspViolationMessage(blockedURI, directive, isReportOnly, originalPolicy);
|
|
56755
|
+
if (isReportOnly) {
|
|
56756
|
+
createConsoleEvent([message], 'warn', { skipStackTrace: true, skipScrubPII: true });
|
|
56757
|
+
}
|
|
56758
|
+
else {
|
|
56759
|
+
createConsoleEvent([message], 'error', { skipStackTrace: true, skipScrubPII: true });
|
|
56760
|
+
}
|
|
56761
|
+
}
|
|
56762
|
+
function createConsoleEvent(args, methodName, { skipStackTrace = false, skipScrubPII = false } = {}) {
|
|
56375
56763
|
if (!args || args.length === 0)
|
|
56376
56764
|
return;
|
|
56377
56765
|
// stringify args
|
|
@@ -56388,8 +56776,11 @@ function ConsoleCapture() {
|
|
|
56388
56776
|
if (!message)
|
|
56389
56777
|
return;
|
|
56390
56778
|
// capture stack trace
|
|
56391
|
-
|
|
56392
|
-
|
|
56779
|
+
let stackTrace = '';
|
|
56780
|
+
if (!skipStackTrace) {
|
|
56781
|
+
const maxStackFrames = methodName === 'error' ? 4 : 1;
|
|
56782
|
+
stackTrace = captureStackTrace(maxStackFrames);
|
|
56783
|
+
}
|
|
56393
56784
|
// truncate message and stack trace
|
|
56394
56785
|
message = truncate(message);
|
|
56395
56786
|
stackTrace = truncate(stackTrace);
|
|
@@ -56400,7 +56791,7 @@ function ConsoleCapture() {
|
|
|
56400
56791
|
return;
|
|
56401
56792
|
}
|
|
56402
56793
|
const devLogEnvelope = createDevLogEnvelope();
|
|
56403
|
-
const consoleEvent = Object.assign(Object.assign({}, devLogEnvelope), { devLogLevel: methodName === 'log' ? 'info' : methodName, devLogMessage: scrubPII(message), devLogTrace: stackTrace, devLogCount: 1 });
|
|
56794
|
+
const consoleEvent = Object.assign(Object.assign({}, devLogEnvelope), { devLogLevel: methodName === 'log' ? 'info' : methodName, devLogMessage: skipScrubPII ? message : scrubPII(message), devLogTrace: stackTrace, devLogCount: 1 });
|
|
56404
56795
|
const wasAccepted = buffer.push(consoleEvent);
|
|
56405
56796
|
if (wasAccepted) {
|
|
56406
56797
|
if (!isPtmPaused) {
|
|
@@ -56462,6 +56853,7 @@ function ConsoleCapture() {
|
|
|
56462
56853
|
pluginAPI.Events.appUnloaded.off(onAppUnloaded);
|
|
56463
56854
|
pluginAPI.Events['ptm:paused'].off(onPtmPaused);
|
|
56464
56855
|
pluginAPI.Events['ptm:unpaused'].off(onPtmUnpaused);
|
|
56856
|
+
pluginAPI.detachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
|
|
56465
56857
|
_.each(CONSOLE_METHODS, function (methodName) {
|
|
56466
56858
|
if (!console[methodName])
|
|
56467
56859
|
return _.noop;
|