Clone Environment- Best practices, troubleshooting methods, code templates

product-replicate-hero.png

Clone Environment Capture best practices:

Following these steps will make sure you get all of your requests on the initial capture:

- Disable all extensions (you can re-enable after capture)

- Close all other instances of Reprise

- Make sure to wait for the capture notification bubble to completely disappear and then count to five before moving on. This makes sure to get all requests and potentially a late requests that didn't load with the initial group of requests. 

- You can open the dev tools network tab during capture to create a HAR file for your entire Clone Environment capture flow. The HAR file can be used to upload and see if there were requests that were missed during the capture process. 

Note: Some applications require the addition of cookies, which can pose a security risk if not managed appropriately.  We recommend capturing on lower environments or using credentials that can be disabled afterwards to thoroughly mitigate any risks.  For more details, feel free to reach out to support@reprise.com for a deeper discussion.

 

The capture didn't work out of the box, what's going on?

These are the most common problems and their fixes:

  1. Request is erroring out (showing red in the network tab) and we didn't get the response on capture

    Fix for 1: we need to recapture that area of the app or use HAR file upload. You can grab the HAR file from the live application in the navigator tab. The HAR file will help us import request responses we may have missed on capture. HAR file upload will also allow us to replace responses we already have with new ones. Check out this video on how to get your HAR file!

  2. Request is erroring out (showing red in the network tab) but we got the response on capture

    Fix for 2: we need to write our code to match the request up with the correct response. This can be usually solved with a pre_process request process (see pre_process templates below) and the appropriate request intercept method. You can also troubleshoot by injecting the response from the live app using a post_process or handle method.

  3. No app-essential requests are failing or the app capture worked for awhile but now it doesn't

    Fix for 3: there might be an auth token in our local/session/cookies that needs to be added and or modified with code to have an indefinite time period. You can add local/session/cookies data via the configure tab directly and you can modify response data with a post_process request processing method (see post_process templates below).

    App-essential requests are usually not image files, font files, analytics requests or other 3rd party requests. While we usually want to fix these at some point, they won't cause a Clone Environment capture to fail completely.
  4. There are no requests failing, the local/session/cookies aren't missing anything app-essential, maybe there's a console error for a js file

    Fix for 4: these are solved by setting break points in the live app and Clone Environment capture and moving through the call stack to find the differences - then correcting them with code. This is going to be solved by the appropriate request intercept method and then a post_process request processing method (see post_process templates). Minified JavaScript files can be quite large so it is best to modify these files vs. entirely replace - even during the troubleshooting process. This is the most difficult and time consuming issue with app capture since js files are large and minified.

 

Troubleshooting steps:

      • Preview the replicate capture, open the live application in another tab or window
      • Open the developer tools, navigate to the network tab (do for live app and demo)
      • Refresh the page with the network tab open to get the full list of the network requests firing to render this page (do for both live app and demo)
      • Look for failing requests (red color in navigator tab of dev tools) - if there are any, we have a 1 or 2 (see fix for 1 and fix for 2 above)
      • Make a determination if this request is app-essential, when in doubt, go ahead and try to fix it
      • To determine if this failing request is a 1 or 2, search the request path in the config tab under web requests, does the request show? If no, our problem is a 1 (see fix for 1 above). If yes, our problem is a 2 (see fix for 2 above)
      • There are no red requests - step through the requests in the demo and the matching request in the live app. Compare the request responses in each if there is a difference, your problem is a 2 (see fix for 2 above)
      • There are no red requests - compare the local/session/cookies of the live app and the replicate capture, looking for keywords such as "auth" or "token". If there are app-essential local/session/cookies missing, you have a 3 (see fix for 3 above)
      • There are no red requests, the local/session/cookies aren't missing anything, the request responses all look correct but we have an error in the console for a js file - in this case, you have a 4 (see fix for 4 above)
      • There are no failing (red color in navigator tab of dev tools) requests, the local/session/cookies aren't missing anything, the request responses all look correct and we have no console errors - in this case you likely have a 4 again (see fix for 4 above)
      • The app capture worked for a few days or hours and now it doesn't - you likely have an auth token that expired and you need to write code to extend it (see fix for 3 above)


Constructing a Code Snippet

Screenshot

This covers how to build HTML Environment Backend code snippets. For Custom JavaScript code snippets, see the "Custom JavaScript" template section. Also, for the full list of methods for B and C, see the Clone Environment documentation.

All replay backend code snippets start with the "replay_backend" prefix (section A). This is the first building block to the code snippet.

You can now choose your request intercept method (section B). The navigator tab in the developer tools will tell you what type of request you're trying to intercept (POST, GET, PUT, PATCH, or DELETE). In your intercept method, you can add logic to filter for a specific request or have no filter to catch all requests with the specified path.

Finally you can pick your processing method (section C). This is the method that will allow you to perform your change to the response of the request. Redirect the request to a response that exists in our backend (what you captured), edit some of the given response, or inject your own response entirely. The interesting thing about this group is that you can stack the pre_process() and post_process() methods (see the template for this in the Other Templates below).

 

Snippet Templates

Based off of the methods in the App Capture API doc https://docs.reprise.com/#introduction 

Most of the edits in app capture are using these code snippet templates with minor adjustments. 

 GET REQUESTS

1) simple GET request intercept

.get("REQUEST_PATH")

2) query string GET request intercept (check entire query string)

.get("REQUEST_PATH", (request) => {
    let url = new URL(request.url);
    return url.search === "?FULL_QUERY_STRING";
})

3) query string GET request intercept (check 1 parameter)

.get("REQUEST_PATH", (request) => {
    let url = new URL(request.url);
    return url.searchParams.get("PARAMETER") === "VALUE";
})

4) query string GET request intercept (check multiple)

.get("REQUEST_PATH", (request) => {
    let url = new URL(request.url);
    return url.searchParams.get("PARAMETER_1") === "VALUE_1" &&
        url.searchParams.get("PARAMETER_2") === "VALUE_2";
})
	

The get() method will not accept query string parameters in the request path that we pass in. If a URL has a "?" in it, the "?" and everything to the right of it denotes the query string parameters and should be excluded from the path parameter we input. This becomes an issue if we have multiple requests with the same path but different query strings. If we just pass the path into our get method like in the code above, we'll intercept all those GET requests despite the different query strings. Then we are performing the same request processing (pre_process, post_process, handle, etc.) on multiple requests, which may not be desirable. 

All that said, we need to add logic to our get method to filter for the GET request with the query string we want. This involves making use of the "request" parameter the get method provides. The request parameter will give us the full request URL of the path we pass. We use then use the URL constructor to parse that URL and then create a filter for parts or the entire query string.

What does this look like in practice?

We'll use the example URL of the request we're trying to intercept as https://abcdefg.preview.app.getreprise.com/myPath?param1=value1&param2=value2

// 1)
.get("/myPath")

// 2) 
.get("/myPath", (request) => {
    let url = new URL(request.url);
    return url.search === "?param1=value1&param2=value2";
})

// 3) 
.get("/myPath", (request) => {
    let url = new URL(request.url);
    return url.searchParams.get("param1") === "value1";
})

// 4) 
.get("/myPath", (request) => {
    let url = new URL(request.url);
    return url.searchParams.get("param1") === "value1" &&
        url.searchParams.get("param1") === "value2";
})

 POST REQUESTS

1) simple POST request intercept

.post("REQUEST_PATH")

2) POST request intercept with json payload query (check 1 key/value)

.post("REQUEST_PATH", (request, body) => {
    let json = JSON.parse(body);
    return json["KEY"] === "VALUE";
})

3) POST request intercept with json payload query

.post("REQUEST_PATH", (request, body) => {
    let json = JSON.parse(body);
    return json["KEY1"] === "VALUE1" && 
        json["KEY2"] === "VALUE2";
})

4) POST request intercept with json payload query (key is a json object)

.post("REQUEST_PATH", (request, body) => {
    let json = JSON.parse(body);
    return JSON.stringify(json["KEY"]) === 
"{KEY1:VALUE1,KEY2:VALUE2}"; })

5) POST request intercept with form data payload query

.post("REQUEST_PATH", (request, body) => {
    return body.includes("STRING");
})

POST requests will contain a payload that we can parse and run a conditional against to filter to only modify the requests we need to modify. Most of the time you'll run into a json payload but you may see a form data payload. Form data payloads can be parsed as a string and you can build a regex to filter for the request you want to modify.

What does this look like in practice?

We'll use the example URL of the request we're trying to intercept as https://abcdefg.preview.app.getreprise.com/myPath
and the following json payload:

{
    "hello": "world",
    "cat": { 
        "color": "black",
        "temperment": "chill"
    }
}

Let's just assume for our purposes that the request also has form data that includes the string "row_123"

// 1)
.post("myPath")

// 2)
.post("myPath", (request, body) => {
    let json = JSON.parse(body);
    return json["hello"] === "world";
})

// 3) 
.post("REQUEST_PATH", (request, body) => {
    let json = JSON.parse(body);
    return json["number"] === 50 && 
        json["hello"] === "world";
})

// 4) 
.post("myPath", (request, body) => {
    let json = JSON.parse(body);
    return JSON.stringify(json["cat"]) === 
'{"color":"black","temperment":"chill"}'; }) // 5) .post("myPath", (request, body) => { return body.includes("row_123"); })

 PRE PROCESS

1) GET intercept - exact match URL in backend

.pre_process(async (request) => {
    const url = new URL(request.url, location.href);
    return new Request(url.toString(), {
        ...request,
        headers: {
            ...request.headers,
            "Explicit-Target": "URL_AS_IT_APPEARS_IN_BACKEND"
        },
        method: request.method,
    });
})

2) GET intercept - match part of request URL

.pre_process(async (request) => {
    const url = new URL(request.url, location.href);
    return new Request(url.toString(), {
        ...request,
        headers: {
            ...request.headers,
            "Partial-Target": "ANY_PART_OF_URL"
        },
        method: request.method,
    });
})

3) POST intercept - payload string matching

.pre_process(async (request) => {
    const body = await request.json();
    const url = new URL(request.url, location.href);
    return new Request(url.toString(), {
        ...request,
        headers: {
            ...request.headers,
            "Custom-Details-Filter": "STRING_IN_PAYLOAD",
            "No-Hash-Check": true
        },
        method: request.method,
        body: JSON.stringify(body),
    });
})

4) POST intercept - grab first matching url in request list (disregard POST data)

.pre_process(async (request) => {
    const body = await request.json();
    const url = new URL(request.url, location.href);
    return new Request(url.toString(), {
        ...request,
        headers: {
            ...request.headers,
            "No-Hash-Check": true
        },
        method: request.method,
        body: JSON.stringify(body),
    });
})
	

Pre-process allows us to choose a response for the particular request we've intercepted. After we've intercepted our broken request, we can point it in the right direction by using filters to point to the response we want in our backend. These filters are called "headers" and we have a few different options to choose from. For example, "Explicit-Target" is kind of like pointing to the response by name (matching the response URL in our backend). Explicit-Target is great for GET requests but usually not as great for POST requests because POST requests don't really differ in name (URL) but instead differ in payload data. POST requests we'd probably want to use "Custom-Details-Filter" because it allows us to search the payload data of a response for a matching string. 

What does this look like in practice?

// 1)
.pre_process(async (request) => {
    const url = new URL(request.url, location.href);
    return new Request(url.toString(), {
        ...request,
        headers: {
            ...request.headers,
            "Explicit-Target": 
"https://abcdefg.preview.app.getreprise.com
/myPath?param1=value1&param2=value2" }, method: request.method, }); }) // 2) .pre_process(async (request) => { const url = new URL(request.url, location.href); return new Request(url.toString(), { ...request, headers: { ...request.headers, "Partial-Target": "/myPath?param1=value1&param2=value2" }, method: request.method, }); }) // 3) .pre_process(async (request) => { const body = await request.json(); const url = new URL(request.url, location.href); return new Request(url.toString(), { ...request, headers: { ...request.headers, "Custom-Details-Filter": "hello-world", "No-Hash-Check": true }, method: request.method, body: JSON.stringify(body), }); }) // 4) .pre_process(async (request) => { const body = await request.json(); const url = new URL(request.url, location.href); return new Request(url.toString(), { ...request, headers: { ...request.headers, "No-Hash-Check": true }, method: request.method, body: JSON.stringify(body), }); })

 POST PROCESS

1) replace string in response


.post_process(async (response) => {
    let html = await response.text();
    html = html.replace('STRING', 'REPLACE_STRING');
    return new Blob([html], {type: "text/html"});
})

2) replace all instances of string in response

.post_process(async (response) => {
    let html = await response.text();
    html = html.replaceAll('STRING', 'REPLACE_STRING');
    return new Blob([html], {type: "text/html"});
})

3) edit value in response json

.post_process(async (response) => {
    let json = await response.json();
    json["KEY"] = "VALUE";
    return new Blob([JSON.stringify(json)],{type:'application/json'});
})

4) send your own json, ignore response json

.post_process(async () => {
    let json = {OBJECT};
    return new Blob([JSON.stringify(json)],{type:'application/json'});
})
	

With post-process, we're not redirecting the request to a different response like in pre-process. Instead, we're modifying the response of this particular request/response pair. We can change parts of html, json, or replace both altogether.

The method of changing the expected window location in a captured JavaScript file to match window origin of the reprise iframe is called an AST (Abstract Syntax Tree) Rewrite. If the code is expecting the window origin of the original app and receives the demo window origin, the JS code may break the demo. AST Rewrite is something that is enabled by default for your instance but there may be situations were certain lines in the javascript aren't being changed correctly. We can manually perform this mutation by intercepting and modifying with a POST PROCESS. We are always looking to change window.top, window.parent, and window.location to window.reprise_top, window.reprise_parent, window.reprise_location. 

If you are finding that the demo capture is still sending requests to the live application, we need to locate the source of the request call and change the live app domain to the captured request domain using "self.origin". Leaving the demo connected to the live application will defeat the purpose of the replicate capture and continue to modify the live app.

What does this look like in practice?

// 1) 
.post_process(async (response) => {
    let html = await response.text();
    html = html.replace('www.website.com', self.origin);
    return new Blob([html], {type: "text/html"});
})

// 2) 
.post_process(async (response) => {
    let html = await response.text();
    html = html.replaceAll('window.top', 'window.reprise_top');
    html = html.replaceAll('window.parent', 'window.reprise_parent');
    html = html.replaceAll('window.location', 'window.reprise_location');
    return new Blob([html], {type: "text/html"});
})

// 3)
.post_process(async (response) => {
    let json = await response.json();
    json["tokenExpireDate"] = Date.now() + 60 * 60 * 1000;
    return new Blob([JSON.stringify(json)],{type:'application/json'});
})

// 4)
.post_process(async () => {
    let json = {"auth":true,"user":"user@user.com"};
    return new Blob([JSON.stringify(json)],{type:'application/json'});
})
  

 HANDLE

1) send your own json, ignore response json

.handle(async ()=>{
    let json = {OBJECT};
    return new Blob([JSON.stringify(json)],{type:'application/json'});
})

2) send your own html, ignore response html

.handle(async ()=>{
    let html = 'HTML_STRING';
    return new Blob([html], {type: "text/html"});
})
	

With handle, we're not redirecting the request to a different response like in pre-process and we're not modifying the given response like post-process. Instead, we're creating new json or html from scratch and returning that. The handle method is the least future proof of pre-process and post-process and should mostly be used as a troubleshooting tool. 

What does this look like in practice?

// 1) 
.handle(async ()=>{
    let json = {"auth":true,"user":"user@user.com"};
    return new Blob([JSON.stringify(json)], {type: 'application/json'})
})

// 2) 
.handle(async ()=>{
let html = '<!doctypehtml>...</script>';
return new Blob([html], {type: "text/html"})
})

 Custom JavaScript

Mutation observer template - use for all Custom JS code

document.addEventListener('DOMContentLoaded', () => {
    const callback = (mutationsList, observer) => {
        let element = document.querySelector('{DOM_element}');
        if (element && !element.hasAttribute('fixed')) {
            
            // code here
 
            element.setAttribute('fixed', 'true');
        }
    };
    const observer = new MutationObserver(callback);
    observer.observe(document.body, {childList:true, subtree:true});
});

With Custom JS, you want to make sure to place your code inside this mutation observer template. Why? Custom JS will fire once, at the start of the demo without this template. Placing your code inside of this mutation observer will allow you to wait to run your code until the target "element" exists on the screen. Checking to see the target is "fixed" allows the mutation to happen again if the user navigates away and comes back to the screen of your target element or elements. 

What does this look like in practice?

// change an image in a replicate capture
document.addEventListener('DOMContentLoaded', () => { const callback = (mutationsList, observer) => { let element = document.querySelector('img[alt="Company Logo"]'); if (element && !element.hasAttribute('fixed')) { element.src =
"https://logos-world.net/wp-content/uploads/2022/05/Acme-Logo.png" element.parentElement.parentElement.style = ""; element.setAttribute('fixed', 'true'); } }; const observer = new MutationObserver(callback); observer.observe(document.body, { childList: true, subtree: true }); }); // change dates to be relevant to now document.addEventListener('DOMContentLoaded', () => { const callback = (mutationsList, observer) => { let element =
document.querySelector('widget[widgetid="5d3b4451758ceb268485fd9a"]'); if (element && !element.hasAttribute('fixed')) { let fixDates = document.querySelectorAll("widget"); console.log(fixDates) fixDates.forEach((e, index) => { let currentDate = new Date(); // Get the current date
// Increase the month by the index value currentDate.setMonth(currentDate.getMonth() + index);
// Add 1 to the month to match the format let month = currentDate.getMonth() + 1;
// Get the full year (YYYY) let year = currentDate.getFullYear(); // Format the date as MM/YYYY e.textContent =
`${month.toString().padStart(2, '0')}/${year}`; }); element.setAttribute('fixed', 'true'); } }; const observer = new MutationObserver(callback); observer.observe(document.body,{childList:true,subtree:true}); });




OTHER COMMONLY USED TEMPLATES

1) block unecessary analytics requests by domain

.match_domain("DOMAIN").block()

2) block by specific request

.get("REQUEST_PATH").block()

3) unblock 3rd party request

.match_domain("DOMAIN").pass()

4) intercept request, redirect to response in backend and then change the data

.get("REQUEST_PATH", (request) => {
    let url = new URL(request.url);
    return url.searchParams.get("PARAMETER") === "VALUE";
})
.pre_process(async (request) => {
    const url = new URL(request.url, location.href);
    return new Request(url.toString(), {
        ...request,
        headers: {
            ...request.headers,
            "Explicit-Target": "URL_AS_IT_APPEARS_IN_BACKEND"
        },
        method: request.method,
    });
}, false)
.post_process(async (response) => {
    let json = await response.json();
    json["KEY"] = "VALUE";
    return JSON.stringify(json);
})
	

 

 

 

 


Was this article helpful?
2 out of 2 found this helpful
Have more questions?
Submit a request
Share it, if you like it.