The stepScreenshotReportGeneratorV1 extension retrieves detailed Virtuoso execution data for a supplied job and journey, converts the executed steps into readable NLP, collects the available before-and-after screenshots, and downloads an optimized PDF report for each journey returned by the execution API.
Each generated report contains a journey summary page followed by screenshot pages showing the checkpoint, screenshot type, step outcome, duration, deparsed step text, used test data, and the captured browser image.
Parameters
-
envrequired, the Virtuoso API environment. Supported values in the current source arePRODUCTION,US, andAPP2. Values are case-sensitive. -
yourTokenrequired, a Virtuoso bearer token with permission to read the target execution, journeys, goals, and language-extension metadata. -
jobIdInputrequired, the completed or sufficiently progressed Virtuoso job execution ID. -
allScreenshotsrequired, a JSON boolean supplied as text. Use"true"to include every available screenshot or"false"to include only screenshots selected by the source filtering rules. -
journeyIdInputrequired, the journey or suite ID used in the detailed execution query. -
logooptional, an SVG string used as the company logo. Pass an empty string to generate the report without a logo.
Security note: Store the bearer token in a Virtuoso environment variable source. Do not hard-code a live token in the extension source, journey, spreadsheets, screenshots, or exported reports.
How the extension works
- Maps
envto a hard-coded Virtuoso API and step-deparser endpoint. - Requests detailed execution data using the supplied job and journey IDs.
- Reads every journey returned in
item.journeys. - Creates one PDF document per returned journey.
- Adds a summary page with journey title, goal, outcome, start date, duration, optional slot/version values, and summary text.
- Deparses executed steps into NLP and resolves names for referenced extensions when possible.
- Selects before and/or after screenshots, compresses them in the browser, and creates one PDF page per selected screenshot.
- Downloads each completed PDF using the sanitized journey title as the filename.
Extension configuration
-
Name:
stepScreenshotReportGeneratorV1 - Run asynchronously: Yes
- Scope: Global
-
Inputs:
env,yourToken,jobIdInput,allScreenshots,journeyIdInput,logo
This extension requires browser-accessible versions of Axios and PDF-Lib. Configure these resources in the extension before execution:
https://cdn.jsdelivr.net/npm/axios/dist/axios.min.jshttps://cdn.jsdelivr.net/npm/pdf-lib/dist/pdf-lib.min.js
Note: Pin approved library versions in controlled environments instead of relying indefinitely on unversioned CDN aliases. Network policy, CSP, proxy rules, or CDN outages can prevent the extension from loading its dependencies.
Use the extension in a journey
Call the extension with values matching the six configured inputs.
stepScreenshotReportGeneratorV1("US", $TokenForPDF, "191276", "true", "7474569", "")In this example, the extension uses the US API, reads job 194276 for journey 747489, includes all available screenshots, and creates the report without a company logo.
Screenshot selection
- When
allScreenshotsis"true", the extension includes every available before or after screenshot. - When it is
"false", a screenshot is included only when the step warnings contain the exact text$ifScreenshot is equal to true. - The value is parsed with
JSON.parse. Use lowercase JSON values"true"or"false". Values such as"Yes","TRUE", or an empty string are treated as false.
Using a logo
The logo input must contain valid SVG markup, not a file path. The source converts the SVG to a PNG at a fixed drawing size before embedding it in each PDF.
stepScreenshotReportGeneratorV1("US", $TokenForPDF, $jobId, "true", $journeyId, $CompanyLogoSvg)Generate multiple PDFs with test data
The current extension inputs are scalar values. To generate reports for multiple executed journeys, data-drive the journey and call the extension once for each test-data row. This keeps every call isolated and reduces the risk of exceeding the asynchronous execution limit.
| env | yourToken | jobIdInput | allScreenshots | journeyIdInput | logo |
|---|---|---|---|---|---|
| US | $TokenForPDF | 1111276 | true | 741489 | |
| US | $TokenForPDF | 1123277 | false | 741490 |
Map the current data row to variables and call:
stepScreenshotReportGeneratorV1($env, $yourToken, $jobIdInput, $allScreenshots, $journeyIdInput, $logo)Important: Use a job ID that belongs to the corresponding journey execution. A mismatched pair can return no journey, the wrong execution context, or an API error. Run the report step only after the target execution has completed and its screenshots and report data are available.
One job containing multiple journeys
The source loops through all journeys returned by the detailed execution response and downloads one PDF per returned journey. Therefore, one row can already create multiple files when the API response contains multiple journeys. Confirm this behavior with a representative job before relying on it because the request is filtered with the supplied journeyIdInput.
Recommended pattern
- Execute the business journey.
- Capture or provide the resulting job ID and journey ID in test data.
- Run the report generator in a separate reporting journey or reporting goal.
- Use one test-data row per report request.
- Keep
allScreenshotsfalse for routine runs and true only when a complete visual record is required. - Split high-volume reporting across multiple jobs or executions instead of processing a very large set in one extension call.
Output
For each successfully processed journey, the extension downloads a file named:
<sanitized_journey_title>_optimized.pdfUnsupported filename characters are replaced with underscores. The extension logs the generated file size and download name to the browser console but does not return the filename, byte size, or success count to a journey variable.
Limitations and operational considerations
- Two-minute execution window: Internal Virtuoso documentation states that an asynchronous extension runs for a maximum of two minutes. Jobs with many journeys, steps, iterations, or screenshots may not finish within this window.
- Cross-browser compatibility: The extension performs PDF generation entirely in the browser using Axios, PDF-Lib, Blob URLs, canvas rendering, image decoding, SVG conversion, and programmatic downloads. These browser APIs can behave differently across Chromium, Firefox, Safari, Edge, mobile browsers, and remote-grid environments. Differences may affect image loading, SVG-to-PNG conversion, canvas memory usage, automatic downloads, filename handling, multiple-file downloads, Blob URL lifetime, and PDF generation performance. The extension should be validated in every browser and device configuration used by the execution plan. Chromium-based execution is recommended as the primary supported configuration unless separate cross-browser validation has been completed.
- No confirmed hard PDF-size limit: Practical limits are instead imposed by browser memory, execution time, image count, image dimensions, download policy, and the remote execution environment.
- Browser memory pressure: Every selected screenshot is fetched into memory, decoded through an image element, drawn to a canvas, converted to JPEG, embedded into PDF-Lib, and retained until the PDF is saved. Large jobs can cause high memory usage, slow garbage collection, browser instability, or out-of-memory termination.
- Page growth: Each selected before screenshot creates one page and each selected after screenshot creates another. A step with both images can therefore add two pages. Enabling all screenshots can increase the PDF rapidly.
- Multiple-download restrictions: The browser may block or prompt for multiple automatic downloads. Remote-grid or cloud executions may not expose downloaded files in the same way as a local interactive browser.
-
Environment support: The current source supports only exact uppercase values
PRODUCTION,US, andAPP2. It has no UK API configuration. The UK Add Extension button installs the extension but does not makeUKa valid runtime value. - Token permissions and expiry: Expired, revoked, or insufficiently privileged tokens cause API failures. The token must also be accepted by the deparser service and any metadata endpoints used during report creation.
- Scalar IDs only: The extension does not parse arrays, comma-separated lists, or JSON collections for job and journey IDs. Passing multiple IDs in one parameter is unsupported without a source-code enhancement.
-
Latest execution dependency: The report is built from each journey's
lastExecution. Running it before execution completion can produce incomplete outcomes, durations, checkpoints, screenshots, or summaries. -
Iteration handling:
includeAllIterationsis hard-coded to false. Only the first checkpoint iteration is processed even though helper functions contain support for multiple iterations, and data-driven executed journeys will not be supported; it always fetches sequence 1 report only. - Screenshot filtering dependency: When all screenshots are disabled, inclusion depends on an exact warning message. Changes to the warning wording or missing warnings can silently exclude screenshots.
- Screenshot access: Screenshot URLs must be reachable from the browser and allowed by CORS, proxy, authentication, and network policy. A failed image request is retried only once after 500 ms.
- Signed-URL cache collision: The image cache removes query parameters from screenshot URLs. Different signed URLs sharing the same path can resolve to the same cache key and may reuse the wrong image.
- Image quality: Screenshots are converted to JPEG with quality selected by pixel area and may be resized. Fine text, anti-aliased controls, or visual defects can lose clarity.
- Unicode and fonts: PDF text uses standard Helvetica fonts. Many unsupported Unicode characters are replaced, transliterated, or removed. Non-Latin scripts and specialized symbols may not render correctly.
- Logo constraints: The logo must be valid browser-renderable SVG. External SVG resources, unsupported fonts, malformed markup, or CSP restrictions can prevent rendering. The fixed canvas size can distort unusual aspect ratios.
- Summary truncation: Long journey summaries are reduced in font size and can be truncated with an ellipsis when they do not fit on the summary page.
- Filename collisions: Journeys with identical sanitized titles produce identical filenames. Browser behavior may overwrite, rename, or reject duplicates.
- Partial-success behavior: Errors while creating one journey PDF are caught and logged, after which processing continues. The overall extension can complete without clearly failing even when one or more PDFs were not generated.
-
No structured return: The extension calls
done()after processing but does not return a machine-readable status, array of filenames, failures, or generated file sizes to subsequent journey steps. -
API-contract dependency: The code assumes specific Virtuoso response structures including
item.journeys,lastExecution, checkpoint report collections, screenshot fields, and step metadata. API schema changes can break the report. - Deparser overhead: Step deparsing is requested while each journey PDF is being prepared and can repeat work for multi-journey jobs, increasing network traffic and runtime.
-
Optional undeclared values: The summary page reads global variables named
slotandversion, but these are not configured extension inputs. They are blank unless those globals happen to exist. - Time-zone behavior: Generated timestamps and formatted start dates use the browser's local time zone, which can differ between local, cloud, and remote-grid executions.
Add the extension to your Virtuoso instance
Select the domain that matches your Virtuoso account. The runtime env value must still be one of the environments supported by the source code.
| Add extension to Virtuoso Production→ | Add extension to Virtuoso Free Trial→ |
| Add extension to Virtuoso US→ | Add extension to Virtuoso UK→ |
View source
Extension name: stepScreenshotReportGeneratorV1
Run asynchronously: Yes
Inputs: env, yourToken, jobIdInput, allScreenshots, journeyIdInput, logo
Resources:
https://cdn.jsdelivr.net/npm/axios/dist/axios.min.jshttps://cdn.jsdelivr.net/npm/pdf-lib/dist/pdf-lib.min.js
const ENVIRONMENT_CONFIG={PRODUCTION:{baseUrl:"https://api.virtuoso.qa/api",stepDeParserUrl:"https://virtuoso-deparser-service-production.apps.virtuoso.qa/webhook",apiDomain:"api.virtuoso.qa"},US:{baseUrl:"https://api-us.virtuoso.qa/api",stepDeParserUrl:"https://virtuoso-deparser-service-production-us.apps-us.virtuoso.qa/webhook",apiDomain:"api-us.virtuoso.qa"},APP2:{baseUrl:"https://api-app2.virtuoso.qa/api",stepDeParserUrl:"https://virtuoso-deparser-service-production.apps.virtuoso.qa/webhook",apiDomain:"api-app2.virtuoso.qa"}},CURRENT_ENVIRONMENT=env,environmentConfig=ENVIRONMENT_CONFIG[CURRENT_ENVIRONMENT],accessToken=yourToken,jobId=jobIdInput,journeyId=journeyIdInput,printAllScreenshots=(()=>{try{return JSON.parse(allScreenshots)}catch(e){return!1}})(),includeAllIterations=!1,slotValue="undefined"!=typeof slot&&slot?String(slot).substring(0,20):"",versionValue="undefined"!=typeof version&&version?String(version).substring(0,20):"",COMPANY_LOGO_SVG=`${logo}`,CHAR_MAP={"▼":"v","▲":"^","◀":"<","▶":">","→":"->","←":"<-","↑":"^","↓":"v","⇣":"[DOWN]","⇓":"[DOWN]","⬇":"[DOWN]","⬍":"[DOWN]","⭣":"[DOWN]","⭳":"[DOWN]","–":"-","—":"-","“":'"',"”":'"',"‘":"'","’":"'","\xa0":" ","…":"...","•":"*","\xa3":"GBP","\xa5":"JPY","₹":"Rs","€":"EUR","\xa9":"(c)","\xae":"(R)","™":"(TM)","₩":"KRW","₽":"RUB","₵":"GHS","₺":"TRY","\xb1":"+/-","\xd7":"x","\xf7":"/","≠":"!=","≤":"<=","≥":">=","∞":"inf","≈":"~","⁄":"/","‹":"<","›":">","\xab":"<<","\xbb":">>"," ":" "," ":" "," ":" ","\xa7":"\xa7","\xb6":"\xb6","†":"†","‡":"‡","\xb0":"deg",µ:"u","‣":">","⁃":"-","⇐":"<==","⇒":"==>","⇧":"^","⇩":"v","↵":"<-'","✓":"√","✗":"x","★":"*","☆":"*","♥":"<3","♠":"^","♦":"<>","♣":"o","\xbc":"1/4","\xbd":"1/2","\xbe":"3/4","⅓":"1/3","⅔":"2/3","\xb2":"2","\xb3":"3","\xb9":"1","⁰":"0","⁴":"4","⁵":"5","\n":" ","\r":" ","\r\n":" "," ":" ","\v":" ","\f":" ","\b":"","\0":"","\n":" ","\r":" "," ":" ","\v":" ","\f":" ","\b":"","\0":"","":"","":"","":"","":"","\uFEFF":"","‐":"-","‑":"-","‒":"-","―":"-","‚":"'","‛":"'","„":'"',"‟":'"',"′":"'","″":'"',"‴":"'''","‵":"`","‶":"``","‷":"```"," ":" "," ":" "," ":" "," ":" "," ":" "," ":" "," ":" "," ":" "," ":" "," ":" "," ":" ","\xad":"","":"","":"","�":"?"},COMPRESSION_CONFIG={smallImageThreshold:1e4,mediumImageThreshold:25e4,smallImage:{quality:.8,maxDimension:1200},mediumImage:{quality:.7,maxDimension:1500},largeImage:{quality:.6,maxDimension:2e3},imageResolutionDPI:150,compressContentStreams:!0,useObjectStreams:!0,removeDuplicateResources:!0,subsetFonts:!0,batchSize:5,maxConcurrentOperations:3},sanitizeTextForPdf=(e,t=!1)=>"string"!=typeof e?String(e):(e=t?e.replace(/\r\n/g,"\n").replace(/\r/g,"\n"):e.replace(/\n/g," ").replace(/\r/g," ")).split("").map(e=>{let r=e.charCodeAt(0);return r>=32&&r<=126||r>=160&&r<=255||CHAR_MAP[e]||t&&"\n"===e?CHAR_MAP[e]||e:" "}).join("");async function convertSvgToPng(e){return new Promise((t,r)=>{let o=new Blob([e],{type:"image/svg+xml"}),a=URL.createObjectURL(o),n=new Image;n.onload=()=>{let e=document.createElement("canvas");e.width=Math.floor(129.06),e.height=Math.floor(51);let o=e.getContext("2d");if(!o){URL.revokeObjectURL(a),r(Error("Failed to get canvas context"));return}o.imageSmoothingEnabled=!0,o.imageSmoothingQuality="high",o.drawImage(n,0,0,e.width,e.height),e.toBlob(e=>{if(!e){URL.revokeObjectURL(a),r(Error("Canvas toBlob returned null"));return}let o=new FileReader;o.onerror=()=>{URL.revokeObjectURL(a),r(Error("FileReader failed"))},o.onloadend=()=>{let e=o.result,r=new Uint8Array(e);URL.revokeObjectURL(a),t(r)},o.readAsArrayBuffer(e)},"image/png",1)},n.onerror=()=>{URL.revokeObjectURL(a),r(Error("Failed to load SVG"))},n.src=a})}const sanitizeTitle=e=>e.replace(/\s+/g,"_").replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").trim().replace(/_+/g,"_"),formatStartDate=e=>{if(null==e)return"N/A";let t=new Date(e);if(isNaN(t.getTime()))return"N/A";let r=e=>e.toString().padStart(2,"0");return`${["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][t.getMonth()]} ${r(t.getDate())}, ${t.getFullYear()} ${r(t.getHours())}:${r(t.getMinutes())}:${r(t.getSeconds())}`},formatDuration=e=>{if(!e||isNaN(e))return"N/A";let t=Math.floor(e/1e3),r,o,a=e=>e.toString().padStart(2,"0");return`${a(Math.floor(t/3600))}:${a(Math.floor(t%3600/60))}:${a(t%60)}`},imageCache=new Map,getImageCacheKey=e=>e.split("?")[0],getUsedDataForStep=(e,t)=>{if(!e||!e.steps)return"No Used Data Available";let r=e.steps[t];return r&&r.sideEffects&&r.sideEffects.usedData?Object.entries(r.sideEffects.usedData).map(([e,t])=>`${e}: ${t}`).join(", ")||"No variables used":"No Used Data Available"};function shouldPrintScreenshot(e,t,r){let o=null!=e.beforeScreenshot,a=null!=e.screenshot;if(!o&&!a)return{shouldPrint:!1,hasBeforeScreenshot:!1,hasAfterScreenshot:!1};if(!r){if(t)return{shouldPrint:o||a,hasBeforeScreenshot:o,hasAfterScreenshot:a};{let n=e.sideEffects?.warnings?.some(e=>e.includes("$ifScreenshot is equal to true"));return{shouldPrint:n&&(o||a),hasBeforeScreenshot:n&&o,hasAfterScreenshot:n&&a}}}let i=t?o||a:(o||a)&&e.sideEffects?.warnings?.some(e=>e.includes("$ifScreenshot is equal to true")),s=e.outcome===r,l=i&&s;return{shouldPrint:l,hasBeforeScreenshot:l&&o,hasAfterScreenshot:l&&a}}async function compressImage(e,t){let r=getImageCacheKey(t);return imageCache.has(r)?imageCache.get(r):new Promise((t,o)=>{try{let a=new Image,n=document.createElement("canvas"),i=n.getContext("2d");if(!i){o(Error("Failed to get canvas 2d context"));return}a.onload=()=>{try{let e=a.width,s=a.height,l=e*s,$;if($=l<COMPRESSION_CONFIG.smallImageThreshold?COMPRESSION_CONFIG.smallImage:l<COMPRESSION_CONFIG.mediumImageThreshold?COMPRESSION_CONFIG.mediumImage:COMPRESSION_CONFIG.largeImage,e>$.maxDimension||s>$.maxDimension){let c=Math.min($.maxDimension/e,$.maxDimension/s);e=Math.floor(e*c),s=Math.floor(s*c)}n.width=e,n.height=s,i.drawImage(a,0,0,e,s);let u=n.toDataURL("image/jpeg",$.quality).split(",")[1],d=atob(u),h=new Uint8Array(d.length);for(let g=0;g<d.length;g++)h[g]=d.charCodeAt(g);imageCache.set(r,h),t(h),URL.revokeObjectURL(a.src),n.width=1,n.height=1}catch(p){URL.revokeObjectURL(a.src),o(p)}},a.onerror=()=>{URL.revokeObjectURL(a.src),o(Error("Failed to load image for compression"))};let s=new Blob([e],{type:"image/png"});a.src=URL.createObjectURL(s)}catch(l){o(l)}})}async function fetchExtensionName(e,t,r){if(!e)return null;if(r.has(e))return r.get(e);let o=`${environmentConfig.baseUrl}/scripts/${e}?includeBody=false&envelope=false`;try{let a=await axios.get(o,{headers:{Authorization:`Bearer ${t}`}});if(a.data&&a.data.id===parseInt(e)){let n=a.data.name;return r.set(e,n),n}return null}catch(i){return console.error(`Error fetching extension name for ID ${e}:`,i),null}}async function fetchGoalName(e,t,r){if(!e)return null;if(r.has(e))return r.get(e);let o=`${environmentConfig.baseUrl}/goals/${e}`;try{let a=await axios.get(o,{headers:{Authorization:`Bearer ${t}`}});if(a.data&&a.data.item&&a.data.item.name){let n=a.data.item.name;return r.set(e,n),n}return null}catch(i){return console.error(`Error fetching goal name for ID ${e}:`,i),null}}async function getStepCheckpointNames(e,t,r=!0){try{if(!e||!e.item||!e.item.journeys)return{allSteps:[],stepsText:{},stepInfoMap:{}};let o=[],a=new Map;Object.values(e.item.journeys).forEach(e=>{let t=e.lastExecution;r?(t&&t.report&&t.report.checkpointsReportsWithIterations&&Object.values(t.report.checkpointsReportsWithIterations).forEach(e=>{e.forEach(e=>{e.steps&&Object.values(e.steps).forEach(e=>{e.step&&(o.push(e.step),a.set(e.step.id,e.step))})})}),t&&t.report&&t.report.libraryCheckpointsReportsWithIterations&&Object.values(t.report.libraryCheckpointsReportsWithIterations).forEach(e=>{e.forEach(e=>{e.steps&&Object.values(e.steps).forEach(e=>{e.step&&(o.push(e.step),a.set(e.step.id,e.step))})})})):(t&&t.report&&t.report.checkpointsReportsWithIterations&&Object.values(t.report.checkpointsReportsWithIterations).forEach(e=>{let t=e[0];t&&t.steps&&Object.values(t.steps).forEach(e=>{e.step&&(o.push(e.step),a.set(e.step.id,e.step))})}),t&&t.report&&t.report.libraryCheckpointsReportsWithIterations&&Object.values(t.report.libraryCheckpointsReportsWithIterations).forEach(e=>{let t=e[0];t&&t.steps&&Object.values(t.steps).forEach(e=>{e.step&&(o.push(e.step),a.set(e.step.id,e.step))})}))});let n=await axios.post(`${environmentConfig.stepDeParserUrl}`,{steps:o},{headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(!n.data)return{allSteps:o,stepsText:{},stepInfoMap:Object.fromEntries(a)};let i={};return o.forEach((e,t)=>{let r=n.data[t];r&&(i[e.id]=r)}),{allSteps:o,stepsText:i,stepInfoMap:Object.fromEntries(a)}}catch(s){return console.error("Error getting step checkpoint names:",s),{allSteps:[],stepsText:{},stepInfoMap:{}}}}async function processStepAndScreenshot(e,t,r,o,a,n,i,s,l,$,c,u,d=!0,h=null,g="after",p=null,f=null){p||(p=await i.embedFont(PDFLib.StandardFonts.Helvetica,{subset:COMPRESSION_CONFIG.subsetFonts})),f||(f=await i.embedFont(PDFLib.StandardFonts.HelveticaBold,{subset:COMPRESSION_CONFIG.subsetFonts}));let m=o[e.stepId]||"No Text Available",b=getUsedDataForStep(r,t),_=sanitizeTextForPdf(b,!0);if(e.step&&"EXECUTE"===e.step.action&&e.step.value&&m.includes("[invalid/deleted script]")){let w=await fetchExtensionName(e.step.value,n,a);w&&(m=m.replace("[invalid/deleted script]",w))}m=sanitizeTextForPdf(m,!0);let x="before"===g?e.beforeScreenshot:e.screenshot;if(x)try{let y=i.addPage(),{width:S,height:C}=y.getSize();y.drawRectangle({x:8,y:8,width:S-16,height:C-16,borderColor:PDFLib.rgb(.4,.4,.4),borderWidth:1.5}),y.drawRectangle({x:14,y:14,width:S-28,height:C-28,borderColor:PDFLib.rgb(0,0,0),borderWidth:.75}),y.drawRectangle({x:0,y:C-70,width:S,height:70,color:PDFLib.rgb(0,0,0),borderColor:PDFLib.rgb(0,0,0),borderWidth:1});let I=0;if(u){let O=50/u.height,E=u.width*O,P=u.height*O;y.drawImage(u,{x:15,y:C-70+(70-P)/2,width:E,height:P}),I=E+25}let T=sanitizeTextForPdf(`${$} - ${c}`),F=S-35-I,N=((e,t,r,o=10,a=20)=>{let n=a;for(;n>=o;){let i=z(p,e,n,t),s=i.length*(n+2),l=!0;for(let $ of i)if(p.widthOfTextAtSize($,n)>t){l=!1;break}if(l&&s<=40)return n;n-=.5}return o})(T,F,40),R=z(p,T,N,F),D=N+2,A=(R.length-1)*D+N,v=C-70,k=v+35+A/2-N;function z(e,t,r,o){let a=t.split(" "),n=[],i=a[0];for(let s=1;s<a.length;s++){let l=a[s],$=i+" "+l;e.widthOfTextAtSize($,r)<=o?i=$:(n.push(i),i=l)}return n.push(i),n}R.forEach((e,t)=>{let r=p.widthOfTextAtSize(e,N),o=I+(S-I-r)/2,a=k-t*D;a>v&&y.drawText(e,{x:o,y:a,size:N,font:p,color:PDFLib.rgb(1,1,1)})});let B=(e,t,r,o,a,n)=>{let i=(e=e.replace(/\n/g," ").replace(/\r/g," ")).split(" "),s="",l=r;for(let $ of i){if(!$)continue;let c=$.split("").map(e=>{let t=e.charCodeAt(0);return t>=32&&t<=126||t>=160&&t<=255||CHAR_MAP[e]?CHAR_MAP[e]||e:" "}).join("");if(!c)continue;let u=s+(s?" ":"")+c;try{if(p.widthOfTextAtSize(u,o)>n&&""!==s){try{y.drawText(s,{x:t,y:l,size:o,font:p,color:PDFLib.rgb(0,0,0)})}catch(d){console.warn("Error drawing text, skipping line:",d)}s=c,l-=a}else s=u}catch(h){console.warn("Error measuring text width, skipping word:",c);continue}}if(s)try{y.drawText(s,{x:t,y:l,size:o,font:p,color:PDFLib.rgb(0,0,0)})}catch(g){console.warn("Error drawing final text line, skipping:",g)}return l},j=C-50-70-15,U=j,M=S-100-120,L=ey(m,10,M),G=ey(_,10,M),W=ey(c,10,M),q=e.outcome||"N/A";ey(q,10,M);let H=(e=>{if(!e||isNaN(e))return"N/A";let t=Math.floor(e/1e3),r=e=>e.toString().padStart(2,"0");return`${r(Math.floor(t/60))}:${r(t%60)}.${(e%1e3).toString().padStart(3,"0")}`})(e.totalDuration);ey(H,10,M);let J=0;d&&null!==h&&(J=30);let V,K=Math.max(30,W+20),Y=Math.max(30,L+20),Q=Math.max(30,G+20),X=K+30+30+30+Y+Q+J,Z=(V=q?.toUpperCase(),"FAIL"===V||"FAILED"===V?PDFLib.rgb(1,.94,.94):"ERROR"===V?PDFLib.rgb(1,.97,.91):null);Z&&y.drawRectangle({x:51,y:U-X+1,width:S-102,height:X-2,color:Z});{let ee=PDFLib.rgb(.95,.96,.97),et=U,er=0,eo=[];J>0&&eo.push(J),eo.push(K,30,30,30,Y,Q),eo.forEach(e=>{er%2==1&&y.drawRectangle({x:51,y:et-e,width:S-102,height:e,color:ee}),et-=e,er++})}y.drawRectangle({x:50,y:U-X,width:S-100,height:X,borderColor:PDFLib.rgb(0,0,0),borderWidth:1}),y.drawLine({start:{x:170,y:U},end:{x:170,y:U-X},thickness:1,color:PDFLib.rgb(0,0,0)});let ea=U;if(d&&null!==h){let en=ea-J;y.drawLine({start:{x:50,y:en},end:{x:S-50,y:en},thickness:1,color:PDFLib.rgb(0,0,0)});let ei=ea-J/2-6;y.drawText("Iteration",{x:60,y:ei,size:12,font:f,color:PDFLib.rgb(0,0,0)}),y.drawText(`${h}`,{x:180,y:ei,size:12,font:p,color:PDFLib.rgb(0,0,0)}),ea=en}let es=ea-K;y.drawLine({start:{x:50,y:es},end:{x:S-50,y:es},thickness:1,color:PDFLib.rgb(0,0,0)});let el=ea-K/2-6;y.drawText("Checkpoint",{x:60,y:el,size:12,font:f,color:PDFLib.rgb(0,0,0)});let e$=ea-(K-W)/2-10;B(sanitizeTextForPdf(c,!0),180,e$,10,12,M-20);let ec=(ea=es)-30;y.drawLine({start:{x:50,y:ec},end:{x:S-50,y:ec},thickness:1,color:PDFLib.rgb(0,0,0)});let eu=ea-15-6;y.drawText("Screenshot Type",{x:60,y:eu,size:12,font:f,color:PDFLib.rgb(0,0,0)});let ed="before"===g?PDFLib.rgb(0,.4,.8):PDFLib.rgb(0,.6,0);y.drawText("before"===g?"Before Element Interaction":"After Element Interaction",{x:180,y:eu,size:12,font:f,color:ed});let eh=(ea=ec)-30;y.drawLine({start:{x:50,y:eh},end:{x:S-50,y:eh},thickness:1,color:PDFLib.rgb(0,0,0)});let eg=ea-15-6;y.drawText("Outcome",{x:60,y:eg,size:12,font:f,color:PDFLib.rgb(0,0,0)});let ep;switch(q?.toUpperCase()){case"PASS":case"PASSED":ep=PDFLib.rgb(0,.5,0);break;case"FAIL":case"FAILED":ep=PDFLib.rgb(.8,0,0);break;case"ERROR":ep=PDFLib.rgb(1,.5,0);break;case"SKIPPED":case"SKIP":ep=PDFLib.rgb(.5,.5,.5);break;default:ep=PDFLib.rgb(0,0,0)}y.drawText(q,{x:180,y:eg,size:12,font:f,color:ep});let ef=(ea=eh)-30;y.drawLine({start:{x:50,y:ef},end:{x:S-50,y:ef},thickness:1,color:PDFLib.rgb(0,0,0)});let e0=ea-15-6;y.drawText("Duration",{x:60,y:e0,size:12,font:f,color:PDFLib.rgb(0,0,0)}),y.drawText(H,{x:180,y:e0,size:12,font:p,color:PDFLib.rgb(0,0,0)});let em=(ea=ef)-Math.max(30,L+20);y.drawLine({start:{x:50,y:em},end:{x:S-50,y:em},thickness:1,color:PDFLib.rgb(0,0,0)});let eb=Math.max(30,L+20),e_=Math.max(30,G+20),ew=ea-eb/2-6;y.drawText("Step",{x:60,y:ew,size:12,font:f,color:PDFLib.rgb(0,0,0)});let ex=ea-(eb-L)/2-10;function ey(e,t,r){return z(p,sanitizeTextForPdf(e,!0),t,r).length*(t+2)}B(m,180,ex,10,12,M-20),y.drawText("Used Data",{x:60,y:em-e_/2-6,size:12,font:f,color:PDFLib.rgb(0,0,0)}),B(_,180,em-(e_-G)/2-10,10,12,M-20),j=U-X-20;let eS;try{eS=await axios.get(x,{responseType:"arraybuffer"})}catch(eC){console.warn("Screenshot fetch failed, retrying once..."),await new Promise(e=>setTimeout(e,500)),eS=await axios.get(x,{responseType:"arraybuffer"})}let eI=eS.data,eO=!1;try{eI=await compressImage(eI,x),eO=!0}catch(eE){console.warn("Image compression failed, using original image")}let eP;if(eO)eP=await i.embedJpg(eI);else{let e3=new Uint8Array(eI);eP=137===e3[0]&&80===e3[1]?await i.embedPng(eI):await i.embedJpg(eI)}let eT=j-50-20,eF=Math.min((S-100)/eP.width,eT/eP.height),eN=eP.width*eF,eR=eP.height*eF;j-=20,y.drawImage(eP,{x:50,y:j-eR,width:eN,height:eR});let eD=`Page ${i.getPageCount()}`,eA=p.widthOfTextAtSize(eD,10);return y.drawText(eD,{x:S-50-eA,y:15,size:10,font:p,color:PDFLib.rgb(0,0,0)}),!0}catch(ev){console.error("Error processing screenshot:",ev)}return!1}async function compressPdf(e){return COMPRESSION_CONFIG.compressContentStreams&&(e.compress=!0),COMPRESSION_CONFIG.useObjectStreams&&(e.useObjectStreams=!0),e}async function processBatch(e,t,r,o,a,n,i,s,l,$=!0,c=null,u=null){let d=[];for(let[h,g,p,f,m]of e){let b=shouldPrintScreenshot(h,n,i),_=0;b.hasBeforeScreenshot&&await processStepAndScreenshot(h,g,p,t,r,o,a,n,i,s,f,l,$,m,"before",c,u)&&_++,b.hasAfterScreenshot&&await processStepAndScreenshot(h,g,p,t,r,o,a,n,i,s,f,l,$,m,"after",c,u)&&_++,d.push(_)}return d}async function processStepsInBatches(e,t,r,o,a,n,i,s,l,$=!0,c=null,u=null){let d=COMPRESSION_CONFIG.batchSize,h=[];for(let g=0;g<e.length;g+=d)h.push(e.slice(g,g+d));let p=0,f=[];for(let m=0;m<h.length;m++){let b=await processBatch(h[m],t,r,o,a,n,i,s,l,$,c,u);b.forEach(e=>{p+=e}),f.push(...b),console.log(`Processed ${p} screenshots so far...`),m<h.length-1&&await new Promise(e=>setTimeout(e,100))}return{results:f,processedCount:p}}async function createSummaryPage(e,t,r,o,a,n,i,s,l="",$="",c=null){let u=await e.embedFont(PDFLib.StandardFonts.Helvetica,{subset:COMPRESSION_CONFIG.subsetFonts}),d=await e.embedFont(PDFLib.StandardFonts.HelveticaBold,{subset:COMPRESSION_CONFIG.subsetFonts}),h=e.addPage(),{width:g,height:p}=h.getSize();h.drawRectangle({x:8,y:8,width:g-16,height:p-16,borderColor:PDFLib.rgb(.4,.4,.4),borderWidth:1.5}),h.drawRectangle({x:14,y:14,width:g-28,height:p-28,borderColor:PDFLib.rgb(0,0,0),borderWidth:.75}),h.drawRectangle({x:0,y:p-70,width:g,height:70,color:PDFLib.rgb(0,0,0),borderColor:PDFLib.rgb(0,0,0),borderWidth:1});let f=0;if(o){let m=50/o.height,b=o.width*m,_=o.height*m;h.drawImage(o,{x:15,y:p-70+(70-_)/2,width:b,height:_}),f=b+25}let w=sanitizeTextForPdf(r),x=g-35-f;function y(e,t,r,o){let a=t.split(" "),n=[],i=a[0];for(let s=1;s<a.length;s++){let l=a[s],$=i+" "+l;e.widthOfTextAtSize($,r)<=o?i=$:(n.push(i),i=l)}return n.push(i),n}let S=((e,t,r,o=10,a=20)=>{let n=a;for(;n>=o;){let i=y(u,e,n,t),s=i.length*(n+2),l=!0;for(let $ of i)if(u.widthOfTextAtSize($,n)>t){l=!1;break}if(l&&s<=40)return n;n-=.5}return o})(w,x,40),C=y(u,w,S,x),I=S+2,O=(C.length-1)*I+S,E=p-70,P=E+35+O/2-S;C.forEach((e,t)=>{let r=f+(g-f-u.widthOfTextAtSize(e,S))/2,o=P-t*I;o>E&&h.drawText(e,{x:r,y:o,size:S,font:u,color:PDFLib.rgb(1,1,1)})});let T=p-70-40,F=12;l&&""!==l.trim()&&(h.drawText("Slot:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf(l),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20),$&&""!==$.trim()&&(h.drawText("Version:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf($),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20),h.drawText("Journey ID:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf(t),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20,h.drawText("Journey Title:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf(r),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20,h.drawText("Goal Name:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf(s),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20,h.drawText("Outcome:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)});let N;switch(n?.toUpperCase()){case"PASS":case"PASSED":N=PDFLib.rgb(0,.5,0);break;case"FAIL":case"FAILED":N=PDFLib.rgb(.8,0,0);break;case"ERROR":N=PDFLib.rgb(1,.5,0);break;case"SKIPPED":case"SKIP":N=PDFLib.rgb(.5,.5,.5);break;case"RUNNING":case"IN_PROGRESS":N=PDFLib.rgb(0,.5,1);break;case"CANCELLED":case"CANCELED":N=PDFLib.rgb(.6,0,.6);break;default:N=PDFLib.rgb(0,0,0)}if(h.drawText(sanitizeTextForPdf(n),{x:200,y:T,size:F,font:d,color:N}),T-=20,c&&(h.drawText("Start Date:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf(c),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20),h.drawText("Duration:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),h.drawText(sanitizeTextForPdf(i),{x:200,y:T,size:F,font:u,color:PDFLib.rgb(0,0,0)}),T-=20,h.drawText("Summary:",{x:50,y:T,size:F,font:d,color:PDFLib.rgb(0,0,0)}),a){let R=T-50-30,D=g-200-50,A=F,v=[],k=0;function z(e,t,r,o){let a=t.split("\n"),n=[];return a.forEach(t=>{if(""===t.trim())n.push("");else{let a=y(e,t,r,o);n.push(...a)}}),n}for(let B=F;B>=8;B-=.5){v=z(u,sanitizeTextForPdf(a,!0),B,D);let j=B+4;if((k=v.length*j)<=R){A=B;break}}if(k>R){let U=Math.floor(R/(A+4));if(U>1){let M=(v=v.slice(0,U-1))[v.length-1];v[v.length-1]=M.substring(0,Math.max(0,M.length-3))+"..."}else U>0&&((v=v.slice(0,1))[0]=v[0].substring(0,Math.max(0,v[0].length-3))+"...")}let L=A+4;v.forEach((e,t)=>{h.drawText(e,{x:200,y:T-t*L,size:A,font:u,color:PDFLib.rgb(0,0,0)})}),T-=v.length*L}else h.drawText("N/A",{x:200,y:T,size:F,font:u,color:PDFLib.rgb(.5,.5,.5)}),T-=20;let G=new Date,W=`Generated on: ${["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][G.getMonth()]} ${G.getDate().toString().padStart(2,"0")}, ${G.getFullYear()} ${G.getHours().toString().padStart(2,"0")}:${G.getMinutes().toString().padStart(2,"0")}:${G.getSeconds().toString().padStart(2,"0")}`;h.drawText(W,{x:50,y:15,size:9,font:u,color:PDFLib.rgb(.4,.4,.4)});let q="Page 1",H=u.widthOfTextAtSize(q,10);h.drawText(q,{x:g-50-H,y:15,size:10,font:u,color:PDFLib.rgb(0,0,0)})}async function processCheckpointsForPdf(e,t,r,o,a,n,i,s,l,$,c,u=!0,d=null,h=null){let g=[];for(let p of e)if(p.canonicalId){let f=t.get(p.canonicalId);if(!f)continue;if(u)f.forEach((e,t)=>{if(e.steps){let r=e.checkpoint?.title||e.title||"Unknown Checkpoint";for(let o in e.steps){let a=e.steps[o];g.push([a,o,e,r,t])}}});else{let m=f[0];if(m&&m.steps){let b=m.checkpoint?.title||m.title||"Unknown Checkpoint";for(let _ in m.steps){let w=m.steps[_];g.push([w,_,m,b,null])}}}}else if(void 0!==p.libraryCheckpointPosition){let x=r.get(p.libraryCheckpointPosition);if(!x)continue;if(u)x.forEach((e,t)=>{if(e.steps){let r=e.checkpoint?.title||e.title||"Unknown Library Checkpoint";for(let o in e.steps){let a=e.steps[o];g.push([a,o,e,r,t])}}});else{let y=x[0];if(y&&y.steps){let S=y.checkpoint?.title||y.title||"Unknown Library Checkpoint";for(let C in y.steps){let I=y.steps[C];g.push([I,C,y,S,null])}}}}let{processedCount:O}=await processStepsInBatches(g,o,a,n,i,s,l,$,c,u,d,h);return O}async function generatePDFs(e,t,r,o=!0,a=null,n=!0){if(console.log("Starting PDF generation with parallel processing..."),!environmentConfig)throw console.error(`Invalid environment: ${CURRENT_ENVIRONMENT}`),Error(`Invalid environment: ${CURRENT_ENVIRONMENT}`);if(!e||!t||!r)throw Error("Missing required parameters: accessToken, jobId, or journeyId");console.log(`Processing job ${t} with journey ${r}...`);let i=[],s=new Map,l=new Map,$=`${environmentConfig.baseUrl}/testsuites/execution?suiteId%5B%5D=${r}&jobId=${t}&detailed=true&includeJourneyDetails=true&includeTrackingDetails=true&includeJobDefinition=true`;try{console.log("Fetching detailed execution data...");let c=await axios.get($,{headers:{Authorization:`Bearer ${e}`}});if(200!==c.status)throw Error(`API call failed with status ${c.status}: ${c.statusText}`);let u=c.data;if(u&&u.item&&u.item.journeys)for(let d in console.log(`Found ${Object.keys(u.item.journeys).length} journeys`),u.item.journeys){let h=u.item.journeys[d];if(!h||!h.journey){console.warn(`Skipping journey ${d}: missing journey data`);continue}let g=h.journey,p=g.goalId,f=h.lastExecution;console.log(`Processing journey: ${g.title||"Untitled"}`);let m=f?.execution?.suiteId||"N/A",b=f?.report?.outcome||f?.statistics?.outcome||"N/A",_=f?.report?.totalDuration||0,w=formatDuration(_),x=await fetchGoalName(p,e,l)||"N/A",y="";g.summary&&(g.summary.summary?y=g.summary.summary:g.summary.aiSummary&&(y=g.summary.aiSummary));try{let{PDFDocument:S}=PDFLib,C=await S.create();C.compress=COMPRESSION_CONFIG.compressContentStreams,C.useObjectStreams=COMPRESSION_CONFIG.useObjectStreams;let I=null;try{console.log("Loading company logo for this PDF...");I=await C.embedPng(await convertSvgToPng(COMPANY_LOGO_SVG))}catch(O){console.error("Failed to load company logo:",O)}let E=f?.execution?.startDate||null,P=E?formatStartDate(E):null;console.log("Creating summary page..."),await createSummaryPage(C,m,g.title,I,y,b,w,x,slotValue,versionValue,P),console.log("Fetching step information...");let{allSteps:T,stepsText:F,stepInfoMap:N}=await getStepCheckpointNames(u,e,n),R=new Map,D=new Map;if(f&&f.report&&f.report.checkpointsReportsWithIterations)for(let A in f.report.checkpointsReportsWithIterations)R.set(A,f.report.checkpointsReportsWithIterations[A]);if(f&&f.report&&f.report.libraryCheckpointsReportsWithIterations)for(let v in f.report.libraryCheckpointsReportsWithIterations)D.set(parseInt(v),f.report.libraryCheckpointsReportsWithIterations[v]);console.log("Processing checkpoints and screenshots with parallel processing...");let k=f&&f.report&&f.report.checkpointsOrder?f.report.checkpointsOrder:[],z,B=await processCheckpointsForPdf(k,R,D,F,s,e,C,o,a,g.title,I,n,await C.embedFont(PDFLib.StandardFonts.Helvetica,{subset:COMPRESSION_CONFIG.subsetFonts}),await C.embedFont(PDFLib.StandardFonts.HelveticaBold,{subset:COMPRESSION_CONFIG.subsetFonts}));console.log(`Processed ${B} screenshots`);let j=C.getPages(),U=j.length,M=await C.embedFont(PDFLib.StandardFonts.Helvetica);j.forEach((e,t)=>{let r=`Page ${t+1} of ${U}`,{width:o}=e.getSize(),a=M.widthOfTextAtSize(r,10),n=o-50-a;e.drawRectangle({x:n-5,y:10,width:a+10,height:20,color:PDFLib.rgb(1,1,1)}),e.drawText(r,{x:n,y:15,size:10,font:M,color:PDFLib.rgb(0,0,0)})}),console.log("Finalizing PDF...");try{C=await compressPdf(C)}catch(L){console.warn("PDF compression failed, continuing with uncompressed PDF")}console.log("Saving PDF document...");let G=await C.save({addDefaultPage:!1,useObjectStreams:!0,objectsPerTick:50});console.log(`PDF created successfully with size: ${Math.round(G.length/1024/1024*100)/100} MB`),console.log(`Developed by
*** * * * * *** ***
* * * * * * * *
*** * * ***** ***** ***
* * * * * * * *
*** *** * * * * ***
`);let W=`${sanitizeTitle(g.title||"Untitled_Journey")}_optimized.pdf`;console.log("Creating download blob...");let q=new Blob([G],{type:"application/pdf"}),H=URL.createObjectURL(q);console.log("Triggering download...");let J=document.createElement("a");J.href=H,J.download=W,J.target="_blank",J.style.display="none",document.body.appendChild(J),await new Promise(e=>setTimeout(e,100)),J.click(),await new Promise(e=>setTimeout(e,500)),document.body.removeChild(J),setTimeout(()=>URL.revokeObjectURL(H),6e4),console.log(`Download triggered for: ${W}`),i.push({title:g.title,fileName:W})}catch(V){console.error(`Error creating PDF for journey "${g.title}":`,V)}if(imageCache.clear(),"function"==typeof gc)try{gc()}catch(K){}}}catch(Y){throw console.error("Error processing job:",Y),Error(`Error processing job ${t}: ${Y.message}`)}return console.log(`Generated ${i.length} optimized PDFs`),i}async function peppaPig(){console.log("Starting PDF generation..."),await generatePDFs(accessToken,jobId,journeyId,printAllScreenshots,null,includeAllIterations)}peppaPig().then(done).catch(doneError);
Comments
0 comments
Please sign in to leave a comment.