How to create AKAMAI CDN Mapping Dashboard

Prev Next

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

  1. 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
    image.png

Indicator

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

Step 2: Create Custom Visualization

  • Navigate to Analysis >> Custom Visualisation >> New
    image.png
  1. 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.
    image.png

Step 4: Create Favorite Chart

  1. 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
      image.png

Step 5: Create Playwright Test

  1. Create Playwright Test: Create a Playwright test with the below script – replace favorite chart ID and the API key.
    image.png

  2. Add the Tracepoints and Indicators created earlier to the Playwright Test’s Insight Section.
    image.png

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

  1. 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]

image.png