@testrelic/maestro-analytics 1.1.0 → 1.2.0
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/README.md +50 -0
- package/dist/cli.cjs +1131 -165
- package/dist/index.cjs +66 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -4
- package/dist/index.d.ts +108 -4
- package/dist/index.js +66 -46
- package/dist/index.js.map +1 -1
- package/dist/merge.cjs +2 -2
- package/dist/merge.cjs.map +1 -1
- package/dist/merge.js +2 -2
- package/dist/merge.js.map +1 -1
- package/dist/proxy-addon/testrelic_capture.py +217 -0
- package/package.json +1 -1
package/dist/merge.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
'use strict';var fs=require('fs'),path=require('path');var
|
|
2
|
-
`);}let
|
|
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
|
package/dist/merge.cjs.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.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
|
|
2
|
-
`);}let
|
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|