@testrelic/maestro-analytics 1.1.0 → 1.2.0-next.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/merge.cjs CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict';var fs=require('fs'),path=require('path');var x={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0};function T(e){let s=0,i=0,n=0,t=0,a=0,c=0,m=0,p=0,l=0,u=e.length,r=0,o={},f=new Set;for(let S of e){f.add(S.url);for(let d of S.tests){switch(s++,d.status){case "passed":i++;break;case "failed":n++;break;case "flaky":t++;break;case "skipped":a++;break;case "timedout":c++;break}if(d.actions)for(let y of d.actions)if(r++,y.category==="assertion")m++,y.status==="passed"?p++:l++;else {let g=y.category;o[g]=(o[g]??0)+1;}}}return {total:s,passed:i,failed:n,flaky:t,skipped:a,timedout:c,totalApiCalls:0,uniqueApiUrls:0,apiCallsByMethod:{},apiCallsByStatusRange:x,apiResponseTime:null,totalAssertions:m,passedAssertions:p,failedAssertions:l,totalNavigations:u,uniqueNavigationUrls:f.size,totalTimelineSteps:e.length,totalActionSteps:r,actionStepsByCategory:o}}function h(e){return "tests"in e&&"visitedAt"in e}function k(e){let s=[],i=[],n="",t="",a="";for(let l of e)try{let u=fs.readFileSync(l,"utf-8"),r=JSON.parse(u);a||(a=r.testRunId),(!n||r.startedAt<n)&&(n=r.startedAt),(!t||r.completedAt&&r.completedAt>t)&&(t=r.completedAt);for(let o of r.timeline)s.push(o),h(o)&&i.push(o);}catch{process.stderr.write(`\u26A0 TestRelic: Unable to read report file: ${l}
2
- `);}let c=T(i),m=n?new Date(n).getTime():Date.now(),p=t?new Date(t).getTime():Date.now();return {schemaVersion:"1.0.0",testRunId:a||`maestro-merged-${Date.now()}`,startedAt:n||new Date().toISOString(),completedAt:t||new Date().toISOString(),totalDuration:p-m,summary:c,ci:null,metadata:null,timeline:s,shardRunIds:null}}function U(e,s){if(!fs.existsSync(e))throw new Error(`Directory does not exist: ${e}`);let i=fs.readdirSync(e).filter(t=>path.extname(t)===".json"&&t.includes("testrelic")).map(t=>path.join(e,t)),n=k(i);return fs.writeFileSync(s,JSON.stringify(n,null,2),"utf-8"),n}exports.mergeReports=k;exports.mergeReportsFromDirectory=U;//# sourceMappingURL=merge.cjs.map
1
+ 'use strict';var fs=require('fs'),path=require('path');var A={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0};function C(t){return t===null?"error":t>=200&&t<300?"2xx":t>=300&&t<400?"3xx":t>=400&&t<500?"4xx":t>=500&&t<600?"5xx":"error"}function d(t,r){if(t.length===0)return null;let i=Math.min(t.length-1,Math.floor(r/100*t.length));return t[i]}function b(t){if(t.length===0)return {totalApiCalls:0,uniqueApiUrls:0,apiCallsByMethod:{},apiCallsByStatusRange:{...A},apiResponseTime:null};let r={},i={...A},s=new Set,e=[];for(let n of t)r[n.method]=(r[n.method]??0)+1,i[C(n.responseStatusCode)]+=1,s.add(n.url),Number.isFinite(n.responseTimeMs)&&n.responseTimeMs>0&&e.push(n.responseTimeMs);e.sort((n,o)=>n-o);let a=e.length>0?{p50:d(e,50)??0,p95:d(e,95)??0,p99:d(e,99)??0,avg:e.reduce((n,o)=>n+o,0)/e.length,min:e[0],max:e[e.length-1]}:null;return {totalApiCalls:t.length,uniqueApiUrls:s.size,apiCallsByMethod:r,apiCallsByStatusRange:i,apiResponseTime:a}}function T(t){let r=0,i=0,s=0,e=0,a=0,n=0,o=0,c=0,m=0,f=t.length,l=0,u={},g=new Set,S=[];for(let R of t){g.add(R.url);for(let p of R.tests){switch(r++,p.status){case "passed":i++;break;case "failed":s++;break;case "flaky":e++;break;case "skipped":a++;break;case "timedout":n++;break}if(p.actions)for(let y of p.actions)if(l++,y.category==="assertion")o++,y.status==="passed"?c++:m++;else {let x=y.category;u[x]=(u[x]??0)+1;}p.apiCalls&&p.apiCalls.length>0&&S.push(...p.apiCalls);}}let h=b(S);return {total:r,passed:i,failed:s,flaky:e,skipped:a,timedout:n,...h,totalAssertions:o,passedAssertions:c,failedAssertions:m,totalNavigations:f,uniqueNavigationUrls:g.size,totalTimelineSteps:t.length,totalActionSteps:l,actionStepsByCategory:u}}function U(t){return "tests"in t&&"visitedAt"in t}function I(t){let r=[],i=[],s="",e="",a="";for(let m of t)try{let f=fs.readFileSync(m,"utf-8"),l=JSON.parse(f);a||(a=l.testRunId),(!s||l.startedAt<s)&&(s=l.startedAt),(!e||l.completedAt&&l.completedAt>e)&&(e=l.completedAt);for(let u of l.timeline)r.push(u),U(u)&&i.push(u);}catch{process.stderr.write(`\u26A0 TestRelic: Unable to read report file: ${m}
2
+ `);}let n=T(i),o=s?new Date(s).getTime():Date.now(),c=e?new Date(e).getTime():Date.now();return {schemaVersion:"1.0.0",testRunId:a||`maestro-merged-${Date.now()}`,startedAt:s||new Date().toISOString(),completedAt:e||new Date().toISOString(),totalDuration:c-o,summary:n,ci:null,metadata:null,timeline:r,shardRunIds:null}}function O(t,r){if(!fs.existsSync(t))throw new Error(`Directory does not exist: ${t}`);let i=fs.readdirSync(t).filter(e=>path.extname(e)===".json"&&e.includes("testrelic")).map(e=>path.join(t,e)),s=I(i);return fs.writeFileSync(r,JSON.stringify(s,null,2),"utf-8"),s}exports.mergeReports=I;exports.mergeReportsFromDirectory=O;//# sourceMappingURL=merge.cjs.map
3
3
  //# sourceMappingURL=merge.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/summary-builder.ts","../src/merge.ts"],"names":["EMPTY_STATUS_RANGE","buildSummary","timeline","total","passed","failed","flaky","skipped","timedout","totalAssertions","passedAssertions","failedAssertions","totalNavigations","totalActionSteps","actionCategoryCounts","uniqueUrls","entry","test","action","cat","isTimelineEntry","item","mergeReports","reportPaths","allTimeline","timelineEntries","earliestStart","latestEnd","runId","reportPath","content","readFileSync","report","summary","startMs","endMs","mergeReportsFromDirectory","dirPath","outputPath","existsSync","readdirSync","f","extname","join","merged","writeFileSync"],"mappings":"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","file":"merge.cjs","sourcesContent":["/**\n * Computes Summary stats from TimelineEntry arrays.\n */\n\nimport type { Summary, TimelineEntry } from '@testrelic/core';\n\ntype ApiCallsByStatusRange = Record<'2xx' | '3xx' | '4xx' | '5xx' | 'error', number>;\n\nconst EMPTY_STATUS_RANGE: ApiCallsByStatusRange = {\n '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0, error: 0,\n};\n\nexport function buildSummary(timeline: readonly TimelineEntry[]): Summary {\n let total = 0;\n let passed = 0;\n let failed = 0;\n let flaky = 0;\n let skipped = 0;\n let timedout = 0;\n let totalAssertions = 0;\n let passedAssertions = 0;\n let failedAssertions = 0;\n const totalNavigations = timeline.length;\n let totalActionSteps = 0;\n const actionCategoryCounts: Record<string, number> = {};\n const uniqueUrls = new Set<string>();\n\n for (const entry of timeline) {\n uniqueUrls.add(entry.url);\n\n for (const test of entry.tests) {\n total++;\n switch (test.status) {\n case 'passed': passed++; break;\n case 'failed': failed++; break;\n case 'flaky': flaky++; break;\n case 'skipped': skipped++; break;\n case 'timedout': timedout++; break;\n }\n\n if (test.actions) {\n for (const action of test.actions) {\n totalActionSteps++;\n if (action.category === 'assertion') {\n totalAssertions++;\n if (action.status === 'passed') passedAssertions++;\n else failedAssertions++;\n } else {\n const cat = action.category;\n actionCategoryCounts[cat] = (actionCategoryCounts[cat] ?? 0) + 1;\n }\n }\n }\n }\n }\n\n return {\n total,\n passed,\n failed,\n flaky,\n skipped,\n timedout,\n totalApiCalls: 0,\n uniqueApiUrls: 0,\n apiCallsByMethod: {},\n apiCallsByStatusRange: EMPTY_STATUS_RANGE,\n apiResponseTime: null,\n totalAssertions,\n passedAssertions,\n failedAssertions,\n totalNavigations,\n uniqueNavigationUrls: uniqueUrls.size,\n totalTimelineSteps: timeline.length,\n totalActionSteps,\n actionStepsByCategory: actionCategoryCounts,\n };\n}\n","/**\n * Merge multiple Maestro report JSON files into a single report.\n */\n\nimport { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, extname } from 'node:path';\nimport type { TestRunReport, TimelineEntry, TimelineStep } from '@testrelic/core';\nimport { buildSummary } from './summary-builder.js';\n\nfunction isTimelineEntry(item: TimelineEntry | TimelineStep): item is TimelineEntry {\n return 'tests' in item && 'visitedAt' in item;\n}\n\nexport function mergeReports(reportPaths: string[]): TestRunReport {\n const allTimeline: (TimelineEntry | TimelineStep)[] = [];\n const timelineEntries: TimelineEntry[] = [];\n let earliestStart = '';\n let latestEnd = '';\n let runId = '';\n\n for (const reportPath of reportPaths) {\n try {\n const content = readFileSync(reportPath, 'utf-8');\n const report = JSON.parse(content) as TestRunReport;\n\n if (!runId) runId = report.testRunId;\n if (!earliestStart || report.startedAt < earliestStart) earliestStart = report.startedAt;\n if (!latestEnd || (report.completedAt && report.completedAt > latestEnd)) latestEnd = report.completedAt;\n\n for (const item of report.timeline) {\n allTimeline.push(item);\n if (isTimelineEntry(item)) timelineEntries.push(item);\n }\n } catch {\n process.stderr.write(`\\u26A0 TestRelic: Unable to read report file: ${reportPath}\\n`);\n }\n }\n\n const summary = buildSummary(timelineEntries);\n const startMs = earliestStart ? new Date(earliestStart).getTime() : Date.now();\n const endMs = latestEnd ? new Date(latestEnd).getTime() : Date.now();\n\n return {\n schemaVersion: '1.0.0',\n testRunId: runId || `maestro-merged-${Date.now()}`,\n startedAt: earliestStart || new Date().toISOString(),\n completedAt: latestEnd || new Date().toISOString(),\n totalDuration: endMs - startMs,\n summary,\n ci: null,\n metadata: null,\n timeline: allTimeline,\n shardRunIds: null,\n };\n}\n\nexport function mergeReportsFromDirectory(dirPath: string, outputPath: string): TestRunReport {\n if (!existsSync(dirPath)) {\n throw new Error(`Directory does not exist: ${dirPath}`);\n }\n\n const reportPaths = readdirSync(dirPath)\n .filter((f) => extname(f) === '.json' && f.includes('testrelic'))\n .map((f) => join(dirPath, f));\n\n const merged = mergeReports(reportPaths);\n writeFileSync(outputPath, JSON.stringify(merged, null, 2), 'utf-8');\n return merged;\n}\n"]}
1
+ {"version":3,"sources":["../src/summary-builder.ts","../src/merge.ts"],"names":["EMPTY_STATUS_RANGE","statusBucket","code","percentile","sorted","p","idx","buildApiStats","calls","byMethod","byStatus","urls","times","c","a","b","apiResponseTime","s","n","buildSummary","timeline","total","passed","failed","flaky","skipped","timedout","totalAssertions","passedAssertions","failedAssertions","totalNavigations","totalActionSteps","actionCategoryCounts","uniqueUrls","allApiCalls","entry","test","action","cat","apiStats","isTimelineEntry","item","mergeReports","reportPaths","allTimeline","timelineEntries","earliestStart","latestEnd","runId","reportPath","content","readFileSync","report","summary","startMs","endMs","mergeReportsFromDirectory","dirPath","outputPath","existsSync","readdirSync","f","extname","join","merged","writeFileSync"],"mappings":"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","file":"merge.cjs","sourcesContent":["/**\n * Computes Summary stats from TimelineEntry arrays.\n */\n\nimport type { Summary, TimelineEntry, ApiCallRecord } from '@testrelic/core';\n\ntype ApiCallsByStatusRange = Record<'2xx' | '3xx' | '4xx' | '5xx' | 'error', number>;\n\nconst EMPTY_STATUS_RANGE: ApiCallsByStatusRange = {\n '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0, error: 0,\n};\n\nfunction statusBucket(code: number | null): keyof ApiCallsByStatusRange {\n if (code === null) return 'error';\n if (code >= 200 && code < 300) return '2xx';\n if (code >= 300 && code < 400) return '3xx';\n if (code >= 400 && code < 500) return '4xx';\n if (code >= 500 && code < 600) return '5xx';\n return 'error';\n}\n\nfunction percentile(sorted: number[], p: number): number | null {\n if (sorted.length === 0) return null;\n const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));\n return sorted[idx];\n}\n\nfunction buildApiStats(calls: ApiCallRecord[]): {\n totalApiCalls: number;\n uniqueApiUrls: number;\n apiCallsByMethod: Record<string, number>;\n apiCallsByStatusRange: ApiCallsByStatusRange;\n apiResponseTime: Summary['apiResponseTime'];\n} {\n if (calls.length === 0) {\n return {\n totalApiCalls: 0,\n uniqueApiUrls: 0,\n apiCallsByMethod: {},\n apiCallsByStatusRange: { ...EMPTY_STATUS_RANGE },\n apiResponseTime: null,\n };\n }\n\n const byMethod: Record<string, number> = {};\n const byStatus: ApiCallsByStatusRange = { ...EMPTY_STATUS_RANGE };\n const urls = new Set<string>();\n const times: number[] = [];\n\n for (const c of calls) {\n byMethod[c.method] = (byMethod[c.method] ?? 0) + 1;\n byStatus[statusBucket(c.responseStatusCode)] += 1;\n urls.add(c.url);\n if (Number.isFinite(c.responseTimeMs) && c.responseTimeMs > 0) {\n times.push(c.responseTimeMs);\n }\n }\n\n times.sort((a, b) => a - b);\n const apiResponseTime: Summary['apiResponseTime'] = times.length > 0 ? {\n p50: percentile(times, 50) ?? 0,\n p95: percentile(times, 95) ?? 0,\n p99: percentile(times, 99) ?? 0,\n avg: times.reduce((s, n) => s + n, 0) / times.length,\n min: times[0],\n max: times[times.length - 1],\n } : null;\n\n return {\n totalApiCalls: calls.length,\n uniqueApiUrls: urls.size,\n apiCallsByMethod: byMethod,\n apiCallsByStatusRange: byStatus,\n apiResponseTime,\n };\n}\n\nexport function buildSummary(timeline: readonly TimelineEntry[]): Summary {\n let total = 0;\n let passed = 0;\n let failed = 0;\n let flaky = 0;\n let skipped = 0;\n let timedout = 0;\n let totalAssertions = 0;\n let passedAssertions = 0;\n let failedAssertions = 0;\n const totalNavigations = timeline.length;\n let totalActionSteps = 0;\n const actionCategoryCounts: Record<string, number> = {};\n const uniqueUrls = new Set<string>();\n const allApiCalls: ApiCallRecord[] = [];\n\n for (const entry of timeline) {\n uniqueUrls.add(entry.url);\n\n for (const test of entry.tests) {\n total++;\n switch (test.status) {\n case 'passed': passed++; break;\n case 'failed': failed++; break;\n case 'flaky': flaky++; break;\n case 'skipped': skipped++; break;\n case 'timedout': timedout++; break;\n }\n\n if (test.actions) {\n for (const action of test.actions) {\n totalActionSteps++;\n if (action.category === 'assertion') {\n totalAssertions++;\n if (action.status === 'passed') passedAssertions++;\n else failedAssertions++;\n } else {\n const cat = action.category;\n actionCategoryCounts[cat] = (actionCategoryCounts[cat] ?? 0) + 1;\n }\n }\n }\n\n if (test.apiCalls && test.apiCalls.length > 0) {\n allApiCalls.push(...test.apiCalls);\n }\n }\n }\n\n const apiStats = buildApiStats(allApiCalls);\n\n return {\n total,\n passed,\n failed,\n flaky,\n skipped,\n timedout,\n ...apiStats,\n totalAssertions,\n passedAssertions,\n failedAssertions,\n totalNavigations,\n uniqueNavigationUrls: uniqueUrls.size,\n totalTimelineSteps: timeline.length,\n totalActionSteps,\n actionStepsByCategory: actionCategoryCounts,\n };\n}\n","/**\n * Merge multiple Maestro report JSON files into a single report.\n */\n\nimport { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, extname } from 'node:path';\nimport type { TestRunReport, TimelineEntry, TimelineStep } from '@testrelic/core';\nimport { buildSummary } from './summary-builder.js';\n\nfunction isTimelineEntry(item: TimelineEntry | TimelineStep): item is TimelineEntry {\n return 'tests' in item && 'visitedAt' in item;\n}\n\nexport function mergeReports(reportPaths: string[]): TestRunReport {\n const allTimeline: (TimelineEntry | TimelineStep)[] = [];\n const timelineEntries: TimelineEntry[] = [];\n let earliestStart = '';\n let latestEnd = '';\n let runId = '';\n\n for (const reportPath of reportPaths) {\n try {\n const content = readFileSync(reportPath, 'utf-8');\n const report = JSON.parse(content) as TestRunReport;\n\n if (!runId) runId = report.testRunId;\n if (!earliestStart || report.startedAt < earliestStart) earliestStart = report.startedAt;\n if (!latestEnd || (report.completedAt && report.completedAt > latestEnd)) latestEnd = report.completedAt;\n\n for (const item of report.timeline) {\n allTimeline.push(item);\n if (isTimelineEntry(item)) timelineEntries.push(item);\n }\n } catch {\n process.stderr.write(`\\u26A0 TestRelic: Unable to read report file: ${reportPath}\\n`);\n }\n }\n\n const summary = buildSummary(timelineEntries);\n const startMs = earliestStart ? new Date(earliestStart).getTime() : Date.now();\n const endMs = latestEnd ? new Date(latestEnd).getTime() : Date.now();\n\n return {\n schemaVersion: '1.0.0',\n testRunId: runId || `maestro-merged-${Date.now()}`,\n startedAt: earliestStart || new Date().toISOString(),\n completedAt: latestEnd || new Date().toISOString(),\n totalDuration: endMs - startMs,\n summary,\n ci: null,\n metadata: null,\n timeline: allTimeline,\n shardRunIds: null,\n };\n}\n\nexport function mergeReportsFromDirectory(dirPath: string, outputPath: string): TestRunReport {\n if (!existsSync(dirPath)) {\n throw new Error(`Directory does not exist: ${dirPath}`);\n }\n\n const reportPaths = readdirSync(dirPath)\n .filter((f) => extname(f) === '.json' && f.includes('testrelic'))\n .map((f) => join(dirPath, f));\n\n const merged = mergeReports(reportPaths);\n writeFileSync(outputPath, JSON.stringify(merged, null, 2), 'utf-8');\n return merged;\n}\n"]}
package/dist/merge.js CHANGED
@@ -1,3 +1,3 @@
1
- import {readFileSync,existsSync,readdirSync,writeFileSync}from'fs';import {extname,join}from'path';var x={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0};function T(e){let s=0,i=0,n=0,t=0,a=0,c=0,m=0,p=0,l=0,u=e.length,r=0,o={},f=new Set;for(let S of e){f.add(S.url);for(let d of S.tests){switch(s++,d.status){case "passed":i++;break;case "failed":n++;break;case "flaky":t++;break;case "skipped":a++;break;case "timedout":c++;break}if(d.actions)for(let y of d.actions)if(r++,y.category==="assertion")m++,y.status==="passed"?p++:l++;else {let g=y.category;o[g]=(o[g]??0)+1;}}}return {total:s,passed:i,failed:n,flaky:t,skipped:a,timedout:c,totalApiCalls:0,uniqueApiUrls:0,apiCallsByMethod:{},apiCallsByStatusRange:x,apiResponseTime:null,totalAssertions:m,passedAssertions:p,failedAssertions:l,totalNavigations:u,uniqueNavigationUrls:f.size,totalTimelineSteps:e.length,totalActionSteps:r,actionStepsByCategory:o}}function h(e){return "tests"in e&&"visitedAt"in e}function k(e){let s=[],i=[],n="",t="",a="";for(let l of e)try{let u=readFileSync(l,"utf-8"),r=JSON.parse(u);a||(a=r.testRunId),(!n||r.startedAt<n)&&(n=r.startedAt),(!t||r.completedAt&&r.completedAt>t)&&(t=r.completedAt);for(let o of r.timeline)s.push(o),h(o)&&i.push(o);}catch{process.stderr.write(`\u26A0 TestRelic: Unable to read report file: ${l}
2
- `);}let c=T(i),m=n?new Date(n).getTime():Date.now(),p=t?new Date(t).getTime():Date.now();return {schemaVersion:"1.0.0",testRunId:a||`maestro-merged-${Date.now()}`,startedAt:n||new Date().toISOString(),completedAt:t||new Date().toISOString(),totalDuration:p-m,summary:c,ci:null,metadata:null,timeline:s,shardRunIds:null}}function U(e,s){if(!existsSync(e))throw new Error(`Directory does not exist: ${e}`);let i=readdirSync(e).filter(t=>extname(t)===".json"&&t.includes("testrelic")).map(t=>join(e,t)),n=k(i);return writeFileSync(s,JSON.stringify(n,null,2),"utf-8"),n}export{k as mergeReports,U as mergeReportsFromDirectory};//# sourceMappingURL=merge.js.map
1
+ import {readFileSync,existsSync,readdirSync,writeFileSync}from'fs';import {extname,join}from'path';var A={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0};function C(t){return t===null?"error":t>=200&&t<300?"2xx":t>=300&&t<400?"3xx":t>=400&&t<500?"4xx":t>=500&&t<600?"5xx":"error"}function d(t,r){if(t.length===0)return null;let i=Math.min(t.length-1,Math.floor(r/100*t.length));return t[i]}function b(t){if(t.length===0)return {totalApiCalls:0,uniqueApiUrls:0,apiCallsByMethod:{},apiCallsByStatusRange:{...A},apiResponseTime:null};let r={},i={...A},s=new Set,e=[];for(let n of t)r[n.method]=(r[n.method]??0)+1,i[C(n.responseStatusCode)]+=1,s.add(n.url),Number.isFinite(n.responseTimeMs)&&n.responseTimeMs>0&&e.push(n.responseTimeMs);e.sort((n,o)=>n-o);let a=e.length>0?{p50:d(e,50)??0,p95:d(e,95)??0,p99:d(e,99)??0,avg:e.reduce((n,o)=>n+o,0)/e.length,min:e[0],max:e[e.length-1]}:null;return {totalApiCalls:t.length,uniqueApiUrls:s.size,apiCallsByMethod:r,apiCallsByStatusRange:i,apiResponseTime:a}}function T(t){let r=0,i=0,s=0,e=0,a=0,n=0,o=0,c=0,m=0,f=t.length,l=0,u={},g=new Set,S=[];for(let R of t){g.add(R.url);for(let p of R.tests){switch(r++,p.status){case "passed":i++;break;case "failed":s++;break;case "flaky":e++;break;case "skipped":a++;break;case "timedout":n++;break}if(p.actions)for(let y of p.actions)if(l++,y.category==="assertion")o++,y.status==="passed"?c++:m++;else {let x=y.category;u[x]=(u[x]??0)+1;}p.apiCalls&&p.apiCalls.length>0&&S.push(...p.apiCalls);}}let h=b(S);return {total:r,passed:i,failed:s,flaky:e,skipped:a,timedout:n,...h,totalAssertions:o,passedAssertions:c,failedAssertions:m,totalNavigations:f,uniqueNavigationUrls:g.size,totalTimelineSteps:t.length,totalActionSteps:l,actionStepsByCategory:u}}function U(t){return "tests"in t&&"visitedAt"in t}function I(t){let r=[],i=[],s="",e="",a="";for(let m of t)try{let f=readFileSync(m,"utf-8"),l=JSON.parse(f);a||(a=l.testRunId),(!s||l.startedAt<s)&&(s=l.startedAt),(!e||l.completedAt&&l.completedAt>e)&&(e=l.completedAt);for(let u of l.timeline)r.push(u),U(u)&&i.push(u);}catch{process.stderr.write(`\u26A0 TestRelic: Unable to read report file: ${m}
2
+ `);}let n=T(i),o=s?new Date(s).getTime():Date.now(),c=e?new Date(e).getTime():Date.now();return {schemaVersion:"1.0.0",testRunId:a||`maestro-merged-${Date.now()}`,startedAt:s||new Date().toISOString(),completedAt:e||new Date().toISOString(),totalDuration:c-o,summary:n,ci:null,metadata:null,timeline:r,shardRunIds:null}}function O(t,r){if(!existsSync(t))throw new Error(`Directory does not exist: ${t}`);let i=readdirSync(t).filter(e=>extname(e)===".json"&&e.includes("testrelic")).map(e=>join(t,e)),s=I(i);return writeFileSync(r,JSON.stringify(s,null,2),"utf-8"),s}export{I as mergeReports,O as mergeReportsFromDirectory};//# sourceMappingURL=merge.js.map
3
3
  //# sourceMappingURL=merge.js.map
package/dist/merge.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/summary-builder.ts","../src/merge.ts"],"names":["EMPTY_STATUS_RANGE","buildSummary","timeline","total","passed","failed","flaky","skipped","timedout","totalAssertions","passedAssertions","failedAssertions","totalNavigations","totalActionSteps","actionCategoryCounts","uniqueUrls","entry","test","action","cat","isTimelineEntry","item","mergeReports","reportPaths","allTimeline","timelineEntries","earliestStart","latestEnd","runId","reportPath","content","readFileSync","report","summary","startMs","endMs","mergeReportsFromDirectory","dirPath","outputPath","existsSync","readdirSync","f","extname","join","merged","writeFileSync"],"mappings":"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","file":"merge.js","sourcesContent":["/**\n * Computes Summary stats from TimelineEntry arrays.\n */\n\nimport type { Summary, TimelineEntry } from '@testrelic/core';\n\ntype ApiCallsByStatusRange = Record<'2xx' | '3xx' | '4xx' | '5xx' | 'error', number>;\n\nconst EMPTY_STATUS_RANGE: ApiCallsByStatusRange = {\n '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0, error: 0,\n};\n\nexport function buildSummary(timeline: readonly TimelineEntry[]): Summary {\n let total = 0;\n let passed = 0;\n let failed = 0;\n let flaky = 0;\n let skipped = 0;\n let timedout = 0;\n let totalAssertions = 0;\n let passedAssertions = 0;\n let failedAssertions = 0;\n const totalNavigations = timeline.length;\n let totalActionSteps = 0;\n const actionCategoryCounts: Record<string, number> = {};\n const uniqueUrls = new Set<string>();\n\n for (const entry of timeline) {\n uniqueUrls.add(entry.url);\n\n for (const test of entry.tests) {\n total++;\n switch (test.status) {\n case 'passed': passed++; break;\n case 'failed': failed++; break;\n case 'flaky': flaky++; break;\n case 'skipped': skipped++; break;\n case 'timedout': timedout++; break;\n }\n\n if (test.actions) {\n for (const action of test.actions) {\n totalActionSteps++;\n if (action.category === 'assertion') {\n totalAssertions++;\n if (action.status === 'passed') passedAssertions++;\n else failedAssertions++;\n } else {\n const cat = action.category;\n actionCategoryCounts[cat] = (actionCategoryCounts[cat] ?? 0) + 1;\n }\n }\n }\n }\n }\n\n return {\n total,\n passed,\n failed,\n flaky,\n skipped,\n timedout,\n totalApiCalls: 0,\n uniqueApiUrls: 0,\n apiCallsByMethod: {},\n apiCallsByStatusRange: EMPTY_STATUS_RANGE,\n apiResponseTime: null,\n totalAssertions,\n passedAssertions,\n failedAssertions,\n totalNavigations,\n uniqueNavigationUrls: uniqueUrls.size,\n totalTimelineSteps: timeline.length,\n totalActionSteps,\n actionStepsByCategory: actionCategoryCounts,\n };\n}\n","/**\n * Merge multiple Maestro report JSON files into a single report.\n */\n\nimport { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, extname } from 'node:path';\nimport type { TestRunReport, TimelineEntry, TimelineStep } from '@testrelic/core';\nimport { buildSummary } from './summary-builder.js';\n\nfunction isTimelineEntry(item: TimelineEntry | TimelineStep): item is TimelineEntry {\n return 'tests' in item && 'visitedAt' in item;\n}\n\nexport function mergeReports(reportPaths: string[]): TestRunReport {\n const allTimeline: (TimelineEntry | TimelineStep)[] = [];\n const timelineEntries: TimelineEntry[] = [];\n let earliestStart = '';\n let latestEnd = '';\n let runId = '';\n\n for (const reportPath of reportPaths) {\n try {\n const content = readFileSync(reportPath, 'utf-8');\n const report = JSON.parse(content) as TestRunReport;\n\n if (!runId) runId = report.testRunId;\n if (!earliestStart || report.startedAt < earliestStart) earliestStart = report.startedAt;\n if (!latestEnd || (report.completedAt && report.completedAt > latestEnd)) latestEnd = report.completedAt;\n\n for (const item of report.timeline) {\n allTimeline.push(item);\n if (isTimelineEntry(item)) timelineEntries.push(item);\n }\n } catch {\n process.stderr.write(`\\u26A0 TestRelic: Unable to read report file: ${reportPath}\\n`);\n }\n }\n\n const summary = buildSummary(timelineEntries);\n const startMs = earliestStart ? new Date(earliestStart).getTime() : Date.now();\n const endMs = latestEnd ? new Date(latestEnd).getTime() : Date.now();\n\n return {\n schemaVersion: '1.0.0',\n testRunId: runId || `maestro-merged-${Date.now()}`,\n startedAt: earliestStart || new Date().toISOString(),\n completedAt: latestEnd || new Date().toISOString(),\n totalDuration: endMs - startMs,\n summary,\n ci: null,\n metadata: null,\n timeline: allTimeline,\n shardRunIds: null,\n };\n}\n\nexport function mergeReportsFromDirectory(dirPath: string, outputPath: string): TestRunReport {\n if (!existsSync(dirPath)) {\n throw new Error(`Directory does not exist: ${dirPath}`);\n }\n\n const reportPaths = readdirSync(dirPath)\n .filter((f) => extname(f) === '.json' && f.includes('testrelic'))\n .map((f) => join(dirPath, f));\n\n const merged = mergeReports(reportPaths);\n writeFileSync(outputPath, JSON.stringify(merged, null, 2), 'utf-8');\n return merged;\n}\n"]}
1
+ {"version":3,"sources":["../src/summary-builder.ts","../src/merge.ts"],"names":["EMPTY_STATUS_RANGE","statusBucket","code","percentile","sorted","p","idx","buildApiStats","calls","byMethod","byStatus","urls","times","c","a","b","apiResponseTime","s","n","buildSummary","timeline","total","passed","failed","flaky","skipped","timedout","totalAssertions","passedAssertions","failedAssertions","totalNavigations","totalActionSteps","actionCategoryCounts","uniqueUrls","allApiCalls","entry","test","action","cat","apiStats","isTimelineEntry","item","mergeReports","reportPaths","allTimeline","timelineEntries","earliestStart","latestEnd","runId","reportPath","content","readFileSync","report","summary","startMs","endMs","mergeReportsFromDirectory","dirPath","outputPath","existsSync","readdirSync","f","extname","join","merged","writeFileSync"],"mappings":"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","file":"merge.js","sourcesContent":["/**\n * Computes Summary stats from TimelineEntry arrays.\n */\n\nimport type { Summary, TimelineEntry, ApiCallRecord } from '@testrelic/core';\n\ntype ApiCallsByStatusRange = Record<'2xx' | '3xx' | '4xx' | '5xx' | 'error', number>;\n\nconst EMPTY_STATUS_RANGE: ApiCallsByStatusRange = {\n '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0, error: 0,\n};\n\nfunction statusBucket(code: number | null): keyof ApiCallsByStatusRange {\n if (code === null) return 'error';\n if (code >= 200 && code < 300) return '2xx';\n if (code >= 300 && code < 400) return '3xx';\n if (code >= 400 && code < 500) return '4xx';\n if (code >= 500 && code < 600) return '5xx';\n return 'error';\n}\n\nfunction percentile(sorted: number[], p: number): number | null {\n if (sorted.length === 0) return null;\n const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));\n return sorted[idx];\n}\n\nfunction buildApiStats(calls: ApiCallRecord[]): {\n totalApiCalls: number;\n uniqueApiUrls: number;\n apiCallsByMethod: Record<string, number>;\n apiCallsByStatusRange: ApiCallsByStatusRange;\n apiResponseTime: Summary['apiResponseTime'];\n} {\n if (calls.length === 0) {\n return {\n totalApiCalls: 0,\n uniqueApiUrls: 0,\n apiCallsByMethod: {},\n apiCallsByStatusRange: { ...EMPTY_STATUS_RANGE },\n apiResponseTime: null,\n };\n }\n\n const byMethod: Record<string, number> = {};\n const byStatus: ApiCallsByStatusRange = { ...EMPTY_STATUS_RANGE };\n const urls = new Set<string>();\n const times: number[] = [];\n\n for (const c of calls) {\n byMethod[c.method] = (byMethod[c.method] ?? 0) + 1;\n byStatus[statusBucket(c.responseStatusCode)] += 1;\n urls.add(c.url);\n if (Number.isFinite(c.responseTimeMs) && c.responseTimeMs > 0) {\n times.push(c.responseTimeMs);\n }\n }\n\n times.sort((a, b) => a - b);\n const apiResponseTime: Summary['apiResponseTime'] = times.length > 0 ? {\n p50: percentile(times, 50) ?? 0,\n p95: percentile(times, 95) ?? 0,\n p99: percentile(times, 99) ?? 0,\n avg: times.reduce((s, n) => s + n, 0) / times.length,\n min: times[0],\n max: times[times.length - 1],\n } : null;\n\n return {\n totalApiCalls: calls.length,\n uniqueApiUrls: urls.size,\n apiCallsByMethod: byMethod,\n apiCallsByStatusRange: byStatus,\n apiResponseTime,\n };\n}\n\nexport function buildSummary(timeline: readonly TimelineEntry[]): Summary {\n let total = 0;\n let passed = 0;\n let failed = 0;\n let flaky = 0;\n let skipped = 0;\n let timedout = 0;\n let totalAssertions = 0;\n let passedAssertions = 0;\n let failedAssertions = 0;\n const totalNavigations = timeline.length;\n let totalActionSteps = 0;\n const actionCategoryCounts: Record<string, number> = {};\n const uniqueUrls = new Set<string>();\n const allApiCalls: ApiCallRecord[] = [];\n\n for (const entry of timeline) {\n uniqueUrls.add(entry.url);\n\n for (const test of entry.tests) {\n total++;\n switch (test.status) {\n case 'passed': passed++; break;\n case 'failed': failed++; break;\n case 'flaky': flaky++; break;\n case 'skipped': skipped++; break;\n case 'timedout': timedout++; break;\n }\n\n if (test.actions) {\n for (const action of test.actions) {\n totalActionSteps++;\n if (action.category === 'assertion') {\n totalAssertions++;\n if (action.status === 'passed') passedAssertions++;\n else failedAssertions++;\n } else {\n const cat = action.category;\n actionCategoryCounts[cat] = (actionCategoryCounts[cat] ?? 0) + 1;\n }\n }\n }\n\n if (test.apiCalls && test.apiCalls.length > 0) {\n allApiCalls.push(...test.apiCalls);\n }\n }\n }\n\n const apiStats = buildApiStats(allApiCalls);\n\n return {\n total,\n passed,\n failed,\n flaky,\n skipped,\n timedout,\n ...apiStats,\n totalAssertions,\n passedAssertions,\n failedAssertions,\n totalNavigations,\n uniqueNavigationUrls: uniqueUrls.size,\n totalTimelineSteps: timeline.length,\n totalActionSteps,\n actionStepsByCategory: actionCategoryCounts,\n };\n}\n","/**\n * Merge multiple Maestro report JSON files into a single report.\n */\n\nimport { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, extname } from 'node:path';\nimport type { TestRunReport, TimelineEntry, TimelineStep } from '@testrelic/core';\nimport { buildSummary } from './summary-builder.js';\n\nfunction isTimelineEntry(item: TimelineEntry | TimelineStep): item is TimelineEntry {\n return 'tests' in item && 'visitedAt' in item;\n}\n\nexport function mergeReports(reportPaths: string[]): TestRunReport {\n const allTimeline: (TimelineEntry | TimelineStep)[] = [];\n const timelineEntries: TimelineEntry[] = [];\n let earliestStart = '';\n let latestEnd = '';\n let runId = '';\n\n for (const reportPath of reportPaths) {\n try {\n const content = readFileSync(reportPath, 'utf-8');\n const report = JSON.parse(content) as TestRunReport;\n\n if (!runId) runId = report.testRunId;\n if (!earliestStart || report.startedAt < earliestStart) earliestStart = report.startedAt;\n if (!latestEnd || (report.completedAt && report.completedAt > latestEnd)) latestEnd = report.completedAt;\n\n for (const item of report.timeline) {\n allTimeline.push(item);\n if (isTimelineEntry(item)) timelineEntries.push(item);\n }\n } catch {\n process.stderr.write(`\\u26A0 TestRelic: Unable to read report file: ${reportPath}\\n`);\n }\n }\n\n const summary = buildSummary(timelineEntries);\n const startMs = earliestStart ? new Date(earliestStart).getTime() : Date.now();\n const endMs = latestEnd ? new Date(latestEnd).getTime() : Date.now();\n\n return {\n schemaVersion: '1.0.0',\n testRunId: runId || `maestro-merged-${Date.now()}`,\n startedAt: earliestStart || new Date().toISOString(),\n completedAt: latestEnd || new Date().toISOString(),\n totalDuration: endMs - startMs,\n summary,\n ci: null,\n metadata: null,\n timeline: allTimeline,\n shardRunIds: null,\n };\n}\n\nexport function mergeReportsFromDirectory(dirPath: string, outputPath: string): TestRunReport {\n if (!existsSync(dirPath)) {\n throw new Error(`Directory does not exist: ${dirPath}`);\n }\n\n const reportPaths = readdirSync(dirPath)\n .filter((f) => extname(f) === '.json' && f.includes('testrelic'))\n .map((f) => join(dirPath, f));\n\n const merged = mergeReports(reportPaths);\n writeFileSync(outputPath, JSON.stringify(merged, null, 2), 'utf-8');\n return merged;\n}\n"]}
@@ -0,0 +1,217 @@
1
+ """
2
+ TestRelic Maestro network-capture addon for mitmproxy.
3
+
4
+ Writes one redacted ApiCallRecord per HTTP transaction to a JSONL file (one
5
+ record per line). The JSONL is later read by the SDK's network-parser, bound
6
+ to Maestro flows by timestamp window, and uploaded as `apiCalls` so the cloud
7
+ platform's Test Detail Network panel and Ask AI `query_network_logs` tool can
8
+ surface the data the same way they do for Playwright.
9
+
10
+ Usage (invoked by the SDK; not run by hand):
11
+
12
+ mitmdump --listen-host 127.0.0.1 --listen-port 8080 \\
13
+ --set testrelic_output=/path/to/network/run.jsonl \\
14
+ --set testrelic_redact_headers=authorization,cookie,set-cookie,x-api-key \\
15
+ --set testrelic_redact_body_fields=password,secret,token,apiKey,api_key \\
16
+ --set testrelic_max_body_bytes=1048576 \\
17
+ -s testrelic_capture.py
18
+
19
+ Notes:
20
+ - Redaction happens here so secrets never touch disk.
21
+ - Bodies above max_body_bytes are truncated (responseBody only) with a
22
+ `responseBodyTruncated=true` flag in the record's `requestHeaders` map (a
23
+ cheap signal — full schema bump can come later).
24
+ - Binary bodies are base64-encoded; matches ApiCallRecord.isBinary semantics.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import base64
30
+ import json
31
+ import re
32
+ import threading
33
+ from datetime import datetime, timezone
34
+ from typing import Any
35
+
36
+ from mitmproxy import ctx, http
37
+
38
+ DEFAULT_REDACT_HEADERS = {"authorization", "cookie", "set-cookie", "x-api-key"}
39
+ DEFAULT_REDACT_BODY_FIELDS = {"password", "secret", "token", "apiKey", "api_key"}
40
+ DEFAULT_MAX_BODY_BYTES = 1024 * 1024
41
+
42
+ REDACTED = "[REDACTED]"
43
+ BINARY_PATTERN = re.compile(
44
+ r"^(image|audio|video|application/(octet-stream|pdf|zip|protobuf))",
45
+ re.IGNORECASE,
46
+ )
47
+
48
+
49
+ class TestRelicCapture:
50
+ def __init__(self) -> None:
51
+ self._lock = threading.Lock()
52
+ self._file = None # type: ignore[var-annotated]
53
+ self._counter = 0
54
+ self._redact_headers: set[str] = set(DEFAULT_REDACT_HEADERS)
55
+ self._redact_body_fields: set[str] = set(DEFAULT_REDACT_BODY_FIELDS)
56
+ self._max_body_bytes: int = DEFAULT_MAX_BODY_BYTES
57
+
58
+ def load(self, loader) -> None: # type: ignore[no-untyped-def]
59
+ loader.add_option("testrelic_output", str, "", "JSONL output path")
60
+ loader.add_option(
61
+ "testrelic_redact_headers", str, "",
62
+ "Comma-separated case-insensitive header names to redact",
63
+ )
64
+ loader.add_option(
65
+ "testrelic_redact_body_fields", str, "",
66
+ "Comma-separated JSON body field names to redact at any depth",
67
+ )
68
+ loader.add_option("testrelic_max_body_bytes", int, DEFAULT_MAX_BODY_BYTES, "Max response body bytes")
69
+
70
+ def running(self) -> None:
71
+ path = ctx.options.testrelic_output
72
+ if not path:
73
+ ctx.log.warn("testrelic_capture: no testrelic_output set; addon idle")
74
+ return
75
+ rh = ctx.options.testrelic_redact_headers
76
+ rb = ctx.options.testrelic_redact_body_fields
77
+ if rh:
78
+ self._redact_headers = {x.strip().lower() for x in rh.split(",") if x.strip()}
79
+ if rb:
80
+ self._redact_body_fields = {x.strip() for x in rb.split(",") if x.strip()}
81
+ self._max_body_bytes = int(ctx.options.testrelic_max_body_bytes or DEFAULT_MAX_BODY_BYTES)
82
+ self._file = open(path, "a", encoding="utf-8", buffering=1)
83
+ ctx.log.info(f"testrelic_capture: writing to {path}")
84
+
85
+ def done(self) -> None:
86
+ if self._file:
87
+ try:
88
+ self._file.close()
89
+ except Exception:
90
+ pass
91
+
92
+ def response(self, flow: http.HTTPFlow) -> None:
93
+ if self._file is None:
94
+ return
95
+ try:
96
+ record = self._build_record(flow)
97
+ line = json.dumps(record, separators=(",", ":"), ensure_ascii=False)
98
+ with self._lock:
99
+ self._file.write(line + "\n")
100
+ except Exception as exc:
101
+ ctx.log.warn(f"testrelic_capture: failed to record {flow.request.pretty_url}: {exc}")
102
+
103
+ def error(self, flow: http.HTTPFlow) -> None:
104
+ # mitmproxy fires this on network errors (DNS, TCP reset, TLS errors etc).
105
+ if self._file is None or flow.response is not None:
106
+ return
107
+ try:
108
+ record = self._build_record(flow, error=str(flow.error) if flow.error else "unknown error")
109
+ line = json.dumps(record, separators=(",", ":"), ensure_ascii=False)
110
+ with self._lock:
111
+ self._file.write(line + "\n")
112
+ except Exception:
113
+ pass
114
+
115
+ # ------------------------------------------------------------------
116
+ # Record builder
117
+ # ------------------------------------------------------------------
118
+
119
+ def _build_record(self, flow: http.HTTPFlow, error: str | None = None) -> dict[str, Any]:
120
+ with self._lock:
121
+ call_id = f"api-call-{self._counter}"
122
+ self._counter += 1
123
+
124
+ ts = datetime.fromtimestamp(flow.request.timestamp_start, tz=timezone.utc).isoformat()
125
+ response_time_ms = 0
126
+ if flow.response and flow.request.timestamp_start and flow.response.timestamp_end:
127
+ response_time_ms = int((flow.response.timestamp_end - flow.request.timestamp_start) * 1000)
128
+
129
+ req_headers = self._redact_header_map(dict(flow.request.headers))
130
+ req_body = self._safe_body(flow.request.content, flow.request.headers.get("content-type"), is_request=True)
131
+
132
+ if error is not None or flow.response is None:
133
+ return {
134
+ "id": call_id,
135
+ "timestamp": ts,
136
+ "method": flow.request.method,
137
+ "url": flow.request.pretty_url,
138
+ "requestHeaders": req_headers,
139
+ "requestBody": req_body,
140
+ "responseStatusCode": None,
141
+ "responseStatusText": None,
142
+ "responseHeaders": None,
143
+ "responseBody": None,
144
+ "responseTimeMs": response_time_ms,
145
+ "isBinary": False,
146
+ "error": error or "no response",
147
+ }
148
+
149
+ res = flow.response
150
+ res_headers = self._redact_header_map(dict(res.headers))
151
+ content_type = res.headers.get("content-type") or ""
152
+ is_binary = bool(BINARY_PATTERN.match(content_type))
153
+ res_body = self._safe_body(res.content, content_type, is_request=False)
154
+
155
+ return {
156
+ "id": call_id,
157
+ "timestamp": ts,
158
+ "method": flow.request.method,
159
+ "url": flow.request.pretty_url,
160
+ "requestHeaders": req_headers,
161
+ "requestBody": req_body,
162
+ "responseStatusCode": res.status_code,
163
+ "responseStatusText": res.reason,
164
+ "responseHeaders": res_headers,
165
+ "responseBody": res_body,
166
+ "responseTimeMs": response_time_ms,
167
+ "isBinary": is_binary,
168
+ "error": None,
169
+ }
170
+
171
+ # ------------------------------------------------------------------
172
+ # Redaction
173
+ # ------------------------------------------------------------------
174
+
175
+ def _redact_header_map(self, headers: dict[str, str]) -> dict[str, str]:
176
+ out: dict[str, str] = {}
177
+ for k, v in headers.items():
178
+ out[k] = REDACTED if k.lower() in self._redact_headers else v
179
+ return out
180
+
181
+ def _redact_json(self, value: Any) -> Any:
182
+ if isinstance(value, dict):
183
+ return {
184
+ k: REDACTED if k in self._redact_body_fields else self._redact_json(v)
185
+ for k, v in value.items()
186
+ }
187
+ if isinstance(value, list):
188
+ return [self._redact_json(v) for v in value]
189
+ return value
190
+
191
+ def _safe_body(self, raw: bytes | None, content_type: str | None, is_request: bool) -> str | None:
192
+ if raw is None or len(raw) == 0:
193
+ return None
194
+
195
+ if not is_request and len(raw) > self._max_body_bytes:
196
+ raw = raw[: self._max_body_bytes]
197
+
198
+ is_binary = bool(content_type and BINARY_PATTERN.match(content_type))
199
+ if is_binary:
200
+ return base64.b64encode(raw).decode("ascii")
201
+
202
+ try:
203
+ text = raw.decode("utf-8")
204
+ except UnicodeDecodeError:
205
+ return base64.b64encode(raw).decode("ascii")
206
+
207
+ # JSON body — apply field-level redaction.
208
+ if content_type and "json" in content_type.lower():
209
+ try:
210
+ parsed = json.loads(text)
211
+ return json.dumps(self._redact_json(parsed), separators=(",", ":"), ensure_ascii=False)
212
+ except Exception:
213
+ return text
214
+ return text
215
+
216
+
217
+ addons = [TestRelicCapture()]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testrelic/maestro-analytics",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-next.52",
4
4
  "description": "Maestro mobile testing analytics — JUnit/artifact parsing, step-level timing, AI defect aggregation, visual regression tracking, and interactive HTML reports",
5
5
  "keywords": [
6
6
  "maestro",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "fast-xml-parser": "^4.5.0",
57
57
  "yaml": "^2.7.0",
58
- "@testrelic/core": "2.4.11"
58
+ "@testrelic/core": "2.7.0-next.52"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^20.0.0",