Overview
The Akamai CDN Mapping dashboard is designed to provide insights into the performance and distribution of content delivery networks (CDNs). This guide will walk you through the steps to create insights, custom visualizations, favorite charts, and Playwright tests for the Akamai CDN Mapping dashboard.
Step 1: Create Insights
- Create Insights: Begin by creating tracepoint for the Akamai CDN Mapping dashboard.
Name = CDN Edge and Test City
Token = city and node
Format = city and HTTP_QUERY

Indicator
Name = Requests [Mapping] and Wait [Mapping]
Token = numreq and wait
Format = &numReq=(\d+) and &wait=(\d+)
Content Type = Number

Step 2: Create Custom Visualization
- Navigate to Analysis >> Custom Visualisation >> New

- Add Scripts: Add the following scripts to the script field:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://vizlibs.catchpoint.com/js/city_continent.js" type="text/javascript" ></script>
<script src="https://vizlibs.catchpoint.com/js/isp_mapping.js" type="text/javascript" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.0.0/d3.min.js" integrity="sha512-il/oXcqETt5LGGmWWbOZLxrDgQQXlr7+ZI37ksA//3e9mxFloAOzlkF8SqtMOlWL6zTCk8hPYUQnreiQvT4elQ==" crossorigin="anonymous"></script>
<script type="text/javascript">
/*
Dimension : Test [Any]
Breakdown 1: Test City [ANY]
Breakdown 2 : CDN Edge [Any]
Metric : 2 LIMIT [wait(ms) , #runs]
Version : v2.4
RAW Data : No
Include Index : No
Request : yes
Summary Data : Hide
URL Params -
Key Example value Description
initial any character To create multiple instances for multiple tests
force true/false To force update configurations this value should be true
theme dark_theme/light_theme Theme of visualization
critical_range any integer e.g.- 300 Critical range of metric
normal_range any integer e.g.- 200 Normal range of Metric
heading Heading text Heading of Visualization
formatted true/false true if all criteria regions should be shown
metric_index 0/1 To configure normal, warning and critical region should be calculated based on what metric
code none/airport_code/locode/azure_code To identify the format of text of cdn cities
viztype POP/CDN_EDGE To show in tooltip text about type of data
*/
$.ajaxSetup({
cache: true
});
document.addEventListener('DOMContentLoaded', function(event) {
var selector = CPVisualization.getContainerSelector();
var summary = CPVisualization.getSummaryData();
var default_values ={
'theme':'light_theme',
'critical_range':200,
'normal_range':150,
'heading':'Akamai CDN Mapping',
'formatted':true,
'metrics':0,
'isp':false,
'code':'none',
'viztype':'CDN_EDGE'
};
var today = new Date();
var random_str = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0);
random_str = random_str.getTime();
$.when(
$.getScript("https://vizlibs.catchpoint.com/custom_libs/cdn_ver_2.4/js/cdn.js?v="+random_str)
).done(function(){
creatCDNVisualization(selector, summary, default_values);
})
});
</script>
<link rel='stylesheet' type='text/css' href='https://vizlibs.catchpoint.com/custom_libs/cdn_ver_2.4/css/cdn.css'>
Step 3: Create the Test unless the Test already exists
- HTTP Test for the targeted CDN request/asset.

Step 4: Create Favorite Chart
- Create Favorite Chart: Create a favorite chart for the test you want to have the mapping dashboard created.
- Visualization: Histogram
- Dimension: Test
- Column: City
- Breakdown: IP
- Metrics: Wait and #Runs

Step 5: Create Playwright Test
-
Create Playwright Test: Create a Playwright test with the below script – replace favorite chart ID and the API key.

-
Add the Tracepoints and Indicators created earlier to the Playwright Test’s Insight Section.

const __translatedScript = async () => { // allow script to be at top for easy edits
await Catchpoint.startStep(`1. Access Token Generation`); /*** Step 1 ***/
// Step - 1
var __url = `http://edc.edgesuite.net/`;
var __response = await page.goto(__url);
var __result = Catchpoint$.runScript(`var xhr = new XMLHttpRequest();xhr.open("POST", 'https://io.catchpoint.com/ui/api/token'); xhr.setRequestHeader("Accept", "*/*"); xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); xhr.send("grant_type=client_credentials&client_id=Rn-s-jXLAuHCGCd&client_secret=150e1cd0-f6b6-401a-8e54-95a18926a6d1");`);
await new Promise(resolve => setTimeout(resolve, Number(3000) )); /* pause */
// Step - 2
await Catchpoint.startStep(`2. City Mapping`); /*** Step 2 ***/
await Catchpoint$.andWait(async _ => {
var __result = Catchpoint$.runScript(`var json = JSON.parse(xhr.response);xhr.open("GET", 'https://io.catchpoint.com/api/v3.4/tests/explorer/favoritechart/data/55314');
xhr.setRequestHeader("Accept", "*/*"); xhr.setRequestHeader("Authorization", "Bearer REPLACE_WITH_API_KEY"); xhr.send();`);
});
await new Promise(resolve => setTimeout(resolve, Number(2000) )); /* pause */
var __result = Catchpoint$.runScript(`var iMap = JSON.parse(xhr.response).data.favoriteCharts[0].summary;var xhr = new XMLHttpRequest();xhr.open("GET", 'http://edc.edgesuite.net/'); xhr.send();for(i=0; i<iMap.items.length;i++) {var xhr = new XMLHttpRequest();xhr.open("GET", 'http://edc.edgesuite.net/?scity='+iMap.items[i].breakdownOne.name+'&wait='+iMap.items[i].syntheticMetrics[0]+'&numReq='+iMap.items[i].syntheticMetrics[1]); xhr.setRequestHeader("X-Forwarded-For", iMap.items[i].breakdownTwo.name); xhr.send(); }`);
await new Promise(resolve => setTimeout(resolve, Number(7000) )); /* pause */
} // end of __translatedScript
///////////////////////////////////////////////
// Translated Script Helpers
class TranslatedScriptHelpers {
logger = this.__cplogger; logs = [];
cdpSession = null; playwright = null;
lastFoundLocator = null; lastFoundFrame = null;
isNextNavigationRootRequest = true; currentStepRootRequest = null;
previousStepRootRequest = null;
originalStartStep = null; currentStepIndex = null;
globalVariables = {}; tracepoints = {};
constructor(logger, playwright, cdpSession) {
this.logger = logger;
this.playwright = playwright;
this.cdpSession = cdpSession;
const __catchpoint = Catchpoint;
this.originalStartStep = __catchpoint.startStep;
__catchpoint.startStep = this.startStep;
}
log = (...args) => {
const formattedLog = args.length > 0 ? args.map(arg => JSON.stringify(arg)).join(' ') : null;
if (formattedLog) {
logger.info(formattedLog);
this.logs.push(new Date().toISOString() + ": " + formattedLog);
}
};
isRequestRootRequest = (request) => {
let result = this.isNextNavigationRootRequest && request.isNavigationRequest() && request.frame() === page.mainFrame();
if (result) {
this.isNextNavigationRootRequest = false;
this.log("isRequestRootRequest found as", request.url());
}
return result;
}
clearLastFound = () => {
this.lastFoundFrame = null;
this.lastFoundLocator = null;
}
andWait = async (actionFunc) => { // wait for navigation after action, up to 2s, otherwise consider it an ajax action
this.log("andWait starting waitForNavigation");
const promise = page.waitForNavigation({ waitUntil: 'commit', timeout: 2000 }).then(() => 'commit').catch(() => 'ajax'); // If it times out, assume AJAX behavior
this.log("andWait awaiting actionFunc");
await actionFunc();
this.log("andWait awaiting waitForNavigation promise");
const result = await promise;
if (result == 'commit') {
this.log("Navigation committed, waiting for load in andWait");
await page.waitForLoadState('load');
this.log("Navigation loaded in andWait");
} else {
this.log("No navigation after wait, assuming AJAX action in andWait");
}
}
storeGlobalVariable = async ({name, value}) => {
if (name) {
if (value?.length > 0) {
this.globalVariables[name] = value;
this.log("Stored global variable", name, "with value", value);
} else {
delete this.globalVariables[name];
this.log("Removed global variable", name, "from results.");
}
}
}
setTracepoint = async ({name, value}) => {
if (name) {
if (value?.length > 0) {
this.tracepoints[name] = value;
this.log("Stored tracepoint", name, "with value", value);
} else {
delete this.tracepoints[name];
this.log("Removed tracepoint", name, "from results.");
}
}
}
finalize = async () => {
for(const [name, value] of Object.entries(this.globalVariables)) {
Catchpoint.storeGlobalVariable(value, name);
}
for(const [name, value] of Object.entries(this.tracepoints)) {
Catchpoint.setTracepoint(name, value);
}
}
startStep = async (...args) => {
await LegacyVar.nextStep(); // Must be before resetResponses!
this.prepareForNextRootRequest();
if (typeof __cpnetworkWatcher !== "undefined") __cpnetworkWatcher?.resetResponses();
if (typeof __cpresponseWatcher !== "undefined") __cpresponseWatcher?.resetResponses();
if (typeof __currentStepOverrides !== "undefined") __currentStepOverrides = null;
this.currentStepIndex = this.currentStepIndex == null ? 0 : this.currentStepIndex + 1;
this.log("Starting step: ", this.currentStepIndex, ...args);
await this.originalStartStep(...args);
}
prepareForNextRootRequest = () => {
this.isNextNavigationRootRequest = true;
this.previousStepRootRequest = this.currentStepRootRequest;
this.currentStepRootRequest = null;
}
setCurrentStepRootRequest = (request) => {
this.currentStepRootRequest = request;
}
sendLogsToWebhook = async (url) => {
if (!url) return;
const rc = await this.playwright["request"]["newContext"]();
const response = await rc.post(url,
{ headers: { 'Content-Type': 'text/plain'}, data: this.logs.join("\n") }
);
};
addMessageToError(err, addedMessage) {
if (err instanceof AggregateError && err.errors.length > 0) {
err = err.errors[0];
}
const { name, message, stack } = err;
err = err.name == "TimeoutError" ? new this.playwright["errors"]["TimeoutError"](message) : new Error(message);
err.stack = stack;
err.message = message.split("\n")[0] + addedMessage;
return err;
}
error(err) { // make errors more relevant to the translated script
if (err instanceof AggregateError && err.errors.length > 0)
err = err.errors[0];
if (err instanceof Error || err.name == "TimeoutError") {
const { name, message, stack } = err;
err = name == "TimeoutError" ? new this.playwright["errors"]["TimeoutError"](message) : new Error(message);
err.name = name;
if (stack) {
const lines = stack.split('\n');
const firstLines = [lines.shift()];
while (lines.length > 0 && !lines[0].match(/\s+at\s/))
firstLines.push(lines.shift());
const scriptIndex = lines.findIndex((l) => l.includes('__translatedScript'));
if (scriptIndex > -1) {
const scriptLine = lines.splice(scriptIndex, 1)[0];
lines.unshift(scriptLine);
}
const modifiedLines = lines.map((line) => line.replace(/executeUserCode/g, 'execute_User_Code')); // workaround
err.stack = [...firstLines, ...modifiedLines].join('\n');
}
}
return err;
}
frameAttributes = async (frame) => {
try {
if (frame.parentFrame()) {
let frameElementHandle = await frame.frameElement();
if (!frameElementHandle) return "Main Frame";
return await frameElementHandle.evaluate((iframeEl) => {
const attrs = [...iframeEl.attributes];
return attrs.reduce((acc, attr) => { acc[attr.name] = attr.value; return acc; }, {});
});
} else return "Main Frame";
} catch(e) { return "Couldn't get frame attributes:" + e.message; }
}
// Use page.locator(xpath/selector) or page.frameLocator(iframeXpath).locator(...) with specific frame for new scripts
anyFrameLocator = async (xpath, options) => { // reproduce selenium behavior of searching all iframes
this.clearLastFound();
const locatorPromises = page.frames().map(async (frame, index) => {
const locator = await frame.locator(xpath, options).first();
if (await locator.count() > 0) {
this.log("Element found for xpath " + xpath + " in frame " + index + " with attributes: " + JSON.stringify(await Catchpoint$.frameAttributes(frame)));
this.lastFoundLocator = locator;
this.lastFoundFrame = frame;
return locator;
}
throw new Error('Element not found in this frame');
});
try { return await Promise.any(locatorPromises); }
catch (e) { throw this.addMessageToError(e, " No element matching selector " + xpath + " found in any frame."); }
};
// Use page.waitForFunction(xpath/selector...) or page.frameLocator(iframeXpath).waitForFunction(...) with specific frame for new scripts
anyFrameWaitForFunction = async (pageFunction, args, options) => { // reproduce selenium behavior of searching all iframes
const startTime = Date.now();
const isNot = options?.isNot || false;
const timeout = options?.timeout || 20000;
this.clearLastFound();
while (true) {
const evaluatePromises = page.frames().map(async (frame, index) => {
if (!frame.isDetached()) {
const ret = await frame.evaluate(pageFunction, args);
this.log("frame", index, "return:", ret);
if(isNot && !ret)
return !ret;
else if(!isNot && ret) {
this.lastFoundFrame = frame;
return ret;
}
throw new Error('evaluate returned falsy');
}
return null;
});
const result = await Promise.any(evaluatePromises).then((value) => { return value; }).catch((error) => { return false; });
if(isNot && !result)
return !result;
else if(!isNot && result)
return result;
if (Date.now() - startTime > timeout) {
const err = new Error("Timeout of " + timeout + "ms exceeded while waiting for " + JSON.stringify(args) + " in any frame.");
err.name = "TimeoutError";
throw err;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
};
let Catchpoint$ = new TranslatedScriptHelpers(this.__cplogger, this.__cpplaywright, this.__cpcdpsession);
const __log = Catchpoint$.log;
__log("TranslatedScriptHelpers loaded");
// Help with idiosyncratic Selenium variable/macro compatibility
class LegacyVar {
static currentStepAssigns = [];
constructor(name) {
this.name = name;
this.cachedValue = '';
}
toString() {
return this.cachedValue;
}
static async new(name, assignment = null) {
let v = new LegacyVar(name);
__log("LegacyVar new: " + name + " from " + assignment.toString());
await v.set(assignment);
return v;
}
async set(macroFunc) {
__log("LegacyVar set called: " + this.name + " from " + macroFunc.toString());
if (typeof macroFunc === 'string' || typeof macroFunc === 'number') {
const value = macroFunc;
macroFunc = async () => value;
}
__log("LegacyVar PREassigned: " + this.name + " from " + macroFunc.toString());
let result = await macroFunc();
__log("LegacyVar (set)assigned: " + this.name + " = " + result + " from " + macroFunc.toString());
this.cachedValue = result;
LegacyVar.currentStepAssigns.push([macroFunc, this]);
return result.toString();
}
get() {
__log("LegacyVar(getting cachedValue): " + this.name + " ? " + this.cachedValue);
return this.cachedValue !== undefined ? this.cachedValue : '';
}
static async refresh() {
__log("LegacyVar refresh called with " + LegacyVar.currentStepAssigns.length + " step assigns");
for ( const [macroFunc, instance] of LegacyVar.currentStepAssigns) {
__log("LegacyVar refresh: " + instance.name + " reassigning from " + macroFunc.toString());
instance.cachedValue = await macroFunc();
__log("LegacyVar refresh: " + instance.name + " = " + instance.cachedValue + " from " + macroFunc.toString());
}
}
static async nextStep() {
LegacyVar.refresh();
LegacyVar.currentStepAssigns = [];
}
}
Catchpoint$.runScript = async (script) => {
Catchpoint$.clearLastFound();
try { return await page.evaluate(script); } catch(e) { Catchpoint$.jsError(e); return null; }
};
Catchpoint$.jsError = (e) => { __log(e); /* TODO: Add to results processing */ }
// End Translated Scipt Helpers
///////////////////////////////////////////////
try { await __translatedScript(); } // run the script at the top
catch(e) { throw Catchpoint$.error(e); }
finally { Catchpoint$.finalize(); }
Step 6: Plot Results Using Custom Visualization
- Plot Results: Plot the results for the above JS API test using custom visualization.
- Disable: Test Data
- Enable: Request Data and add filters to select the request – http://edc.edgesuite.net/
- Dimensions: Request
- Column: CDN Edge
- Breakdown: Test City
- Plot for the indicators: Wait[Mapping], Requests[Mapping]
