@interfere/test-utils 10.1.0 → 10.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @interfere/test-utils
2
2
 
3
+ ## 10.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 58353d7: The telemetry receiver now saves the response body of each forwarded request that gets a non-2xx response, in the new optional `response` field of `ReceivedRequest`. A forwarded request that throws is now recorded as 500 instead of 200.
8
+
9
+ ## 10.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [76a8c1e]
14
+ - @interfere/helpers@9.3.3
15
+
3
16
  ## 10.1.0
4
17
 
5
18
  ### Minor Changes
@@ -1,2 +1,2 @@
1
- import{l as afterEach,t as beforeEach,z as test}from"../node_modules/.bun/vitest@5.1.0_c373f03cbec31808/node_modules/vitest/dist/chunks/run.CjdgFZiW.mjs";import{g as globalExpect,v as vi}from"../node_modules/.bun/vitest@5.1.0_c373f03cbec31808/node_modules/vitest/dist/chunks/index.CZzC1gGg.mjs";import{exportedSpans}from"./otlp.mjs";import{receiver}from"./telemetry-receiver.mjs";function serverContract(load){let collector,adapter,originalConsole={...console};beforeEach(async()=>{vi.resetModules(),collector=await receiver(),adapter=await load(collector.url)}),afterEach(async()=>{Object.assign(console,originalConsole),await adapter.dispose(),await collector.close(),vi.unstubAllEnvs(),vi.restoreAllMocks()}),test(`concurrent registration has one owner and exports each log once`,async()=>{globalExpect.hasAssertions(),await Promise.all([adapter.register(),adapter.register(),adapter.register()]),console.info(`server-contract-once`),await Promise.resolve(),await adapter.flush();let bodies=collector.requests.filter(request=>request.url.includes(`logs`)).map(request=>request.body).join(`
1
+ import{l as afterEach,t as beforeEach,z as test}from"../node_modules/.bun/vitest@5.1.0_6d49a524c68e2bf6/node_modules/vitest/dist/chunks/run.CjdgFZiW.mjs";import{g as globalExpect,v as vi}from"../node_modules/.bun/vitest@5.1.0_6d49a524c68e2bf6/node_modules/vitest/dist/chunks/index.CZzC1gGg.mjs";import{exportedSpans}from"./otlp.mjs";import{receiver}from"./telemetry-receiver.mjs";function serverContract(load){let collector,adapter,originalConsole={...console};beforeEach(async()=>{vi.resetModules(),collector=await receiver(),adapter=await load(collector.url)}),afterEach(async()=>{Object.assign(console,originalConsole),await adapter.dispose(),await collector.close(),vi.unstubAllEnvs(),vi.restoreAllMocks()}),test(`concurrent registration has one owner and exports each log once`,async()=>{globalExpect.hasAssertions(),await Promise.all([adapter.register(),adapter.register(),adapter.register()]),console.info(`server-contract-once`),await Promise.resolve(),await adapter.flush();let bodies=collector.requests.filter(request=>request.url.includes(`logs`)).map(request=>request.body).join(`
2
2
  `);globalExpect(bodies.match(/server-contract-once/gu)).toHaveLength(1)}),test(`preserves application ownership while delivering captured errors once`,async()=>{let host=adapter.installHost();await Promise.all([adapter.register(),adapter.register()]),host.run(()=>adapter.capture(/* @__PURE__ */ Error(`host-contract-error`))),await adapter.flush(),globalExpect(host.ownsTracing()).toBe(!0),globalExpect(host.captured()).toBe(1),globalExpect(JSON.stringify(exportedSpans(collector.requests)).match(/host-contract-error/gu)?.length).toBeGreaterThan(0),globalExpect(exportedSpans(collector.requests)).toHaveLength(1)}),test(`redacts sensitive URLs before exporting spans and captured exceptions`,async()=>{globalExpect.hasAssertions(),await adapter.register(),adapter.recordUrl(`https://service.example/orders?token=contract-secret&sort=asc`),adapter.capture(/* @__PURE__ */ Error(`failed https://service.example/orders?token=contract-secret&sort=asc`)),await adapter.flush();let spans=exportedSpans(collector.requests),body=JSON.stringify(spans);globalExpect(spans.length).toBeGreaterThanOrEqual(2),globalExpect(body).not.toContain(`contract-secret`),globalExpect(body).toContain(`REDACTED`),globalExpect(body).toContain(`sort=asc`)})}export{serverContract};
@@ -4,6 +4,7 @@ export interface ReceivedRequest {
4
4
  headers: Headers;
5
5
  body: string;
6
6
  status: number;
7
+ response?: string;
7
8
  }
8
9
  export declare function receiver(options?: {
9
10
  readonly sdkConfig?: Readonly<Record<string, string>>;
@@ -1 +1 @@
1
- import{readFile}from"node:fs/promises";import{extname,resolve,sep}from"node:path";import{once}from"node:events";import{HttpResponse,getResponse,http}from"msw";import{buffer}from"node:stream/consumers";import{createServer}from"node:http";import{setTimeout}from"node:timers/promises";import{gunzipSync}from"node:zlib";async function receiver(options={}){let requests=[],failures=/* @__PURE__ */ new Map,stalls=/* @__PURE__ */ new Map,assets,upstream;async function resolveResponse(request,incoming){let url=new URL(request.url),{headers}=request,responseHeaders=new Headers({"access-control-allow-origin":`*`,"access-control-allow-headers":headers.get(`access-control-request-headers`)??`*`,"access-control-allow-methods":`GET, POST, OPTIONS`});try{if(request.method===`OPTIONS`)return new HttpResponse(null,{status:204,headers:responseHeaders});if(assets&&request.method===`GET`&&!url.pathname.startsWith(`/v3/`)){let file=resolve(assets,`.${url.pathname===`/`?`/index.html`:url.pathname}`);return file.startsWith(`${resolve(assets)}${sep}`)?(responseHeaders.set(`content-type`,extname(file)===`.html`?`text/html`:`text/javascript`),new HttpResponse(await readFile(file),{headers:responseHeaders})):new HttpResponse(null,{status:403,headers:responseHeaders})}let stall=[...stalls].find(([prefix])=>url.pathname.startsWith(prefix))?.[1];stall!==void 0&&(incoming.pause(),await setTimeout(stall),incoming.resume());let raw=await buffer(incoming),decoded=headers.get(`content-encoding`)===`gzip`?gunzipSync(raw):raw,body=decoded.toString(),failure=[...failures].find(([prefix])=>url.pathname.startsWith(prefix))?.[1],entry={method:request.method,url:url.pathname+url.search,headers,body,status:200};if(requests.push(entry),failure&&failure.remaining>0)return--failure.remaining,entry.status=failure.status,responseHeaders.set(`content-type`,`application/json`),new HttpResponse([204,205,304].includes(failure.status)?null:failure.body,{status:failure.status,headers:responseHeaders});if(upstream){let init={method:entry.method,headers};raw.length>0&&(init.body=raw);let result=await upstream(new Request(url,init));entry.status=result.status;for(let[key,value]of result.headers)responseHeaders.set(key,value);return new HttpResponse(result.body===null?null:await result.arrayBuffer(),{status:result.status,headers:responseHeaders})}let custom=options.respond?.(entry,decoded);if(custom){entry.status=custom.status;for(let[key,value]of responseHeaders)custom.headers.set(key,value);return custom}return url.pathname===`/v3/releases/config`?HttpResponse.json({org:{id:`org_acceptance`,slug:`acceptance`},surface:{id:`surface_acceptance`,slug:`web`,sourceProvider:`github`,destinationProvider:`cli`,environmentFiltering:!0,sdkConfig:null}},{headers:responseHeaders}):HttpResponse.json(url.pathname.includes(`config`)?{plugins:{errors:!0,logs:!0,device:!0,pageEvents:!0,rageClick:!0,replay:!0},...options.sdkConfig}:{},{headers:responseHeaders})}catch(error){return new HttpResponse(new TextEncoder().encode(String(error)),{status:500,headers:responseHeaders})}}async function handle(request,response){try{let headers=new Headers;for(let[key,value]of Object.entries(request.headers))value!==void 0&&headers.set(key,Array.isArray(value)?value.join(`, `):value);let init={method:request.method??`GET`,headers},resolved=await getResponse([http.all(`*`,async({request:intercepted})=>resolveResponse(intercepted,request))],new Request(new URL(request.url??`/`,`http://localhost`),init));if(!resolved)throw Error(`Missing telemetry receiver response`);for(let[key,value]of resolved.headers)response.setHeader(key,value);response.writeHead(resolved.status).end(Buffer.from(await resolved.arrayBuffer()))}catch(error){response.destroy(error instanceof Error?error:Error(String(error)))}}let server=createServer((request,response)=>{handle(request,response)});server.listen(0,`127.0.0.1`),await once(server,`listening`);let address=server.address();if(!(address instanceof Object))throw TypeError(`Receiver did not bind a TCP port`);return{url:`http://127.0.0.1:${address.port}`,requests,serve(directory){assets=directory},forward(handler){upstream=handler},reset(){requests.length=0,failures.clear(),stalls.clear()},fail(path,status,attempts=1,body=`{}`){failures.set(path,{status,remaining:attempts,body})},stall(path,ms){stalls.set(path,ms)},async close(){server.closeAllConnections(),await new Promise((resolve,reject)=>{server.close(error=>{error?reject(error):resolve()})})}}}export{receiver};
1
+ import{readFile}from"node:fs/promises";import{extname,resolve,sep}from"node:path";import{once}from"node:events";import{HttpResponse,getResponse,http}from"msw";import{buffer}from"node:stream/consumers";import{createServer}from"node:http";import{setTimeout}from"node:timers/promises";import{gunzipSync}from"node:zlib";async function receiver(options={}){let requests=[],failures=/* @__PURE__ */ new Map,stalls=/* @__PURE__ */ new Map,assets,upstream;async function resolveResponse(request,incoming){let url=new URL(request.url),{headers}=request,responseHeaders=new Headers({"access-control-allow-origin":`*`,"access-control-allow-headers":headers.get(`access-control-request-headers`)??`*`,"access-control-allow-methods":`GET, POST, OPTIONS`});try{if(request.method===`OPTIONS`)return new HttpResponse(null,{status:204,headers:responseHeaders});if(assets&&request.method===`GET`&&!url.pathname.startsWith(`/v3/`)){let file=resolve(assets,`.${url.pathname===`/`?`/index.html`:url.pathname}`);return file.startsWith(`${resolve(assets)}${sep}`)?(responseHeaders.set(`content-type`,extname(file)===`.html`?`text/html`:`text/javascript`),new HttpResponse(await readFile(file),{headers:responseHeaders})):new HttpResponse(null,{status:403,headers:responseHeaders})}let stall=[...stalls].find(([prefix])=>url.pathname.startsWith(prefix))?.[1];stall!==void 0&&(incoming.pause(),await setTimeout(stall),incoming.resume());let raw=await buffer(incoming),decoded=headers.get(`content-encoding`)===`gzip`?gunzipSync(raw):raw,body=decoded.toString(),failure=[...failures].find(([prefix])=>url.pathname.startsWith(prefix))?.[1],entry={method:request.method,url:url.pathname+url.search,headers,body,status:200};if(requests.push(entry),failure&&failure.remaining>0)return--failure.remaining,entry.status=failure.status,responseHeaders.set(`content-type`,`application/json`),new HttpResponse([204,205,304].includes(failure.status)?null:failure.body,{status:failure.status,headers:responseHeaders});if(upstream){let init={method:entry.method,headers};raw.length>0&&(init.body=raw),entry.status=500;let result=await upstream(new Request(url,init));entry.status=result.status;for(let[key,value]of result.headers)responseHeaders.set(key,value);let payload=result.body===null?null:await result.arrayBuffer();return(result.status<200||result.status>=300)&&payload&&(entry.response=new TextDecoder().decode(payload)),new HttpResponse(payload,{status:result.status,headers:responseHeaders})}let custom=options.respond?.(entry,decoded);if(custom){entry.status=custom.status;for(let[key,value]of responseHeaders)custom.headers.set(key,value);return custom}return url.pathname===`/v3/releases/config`?HttpResponse.json({org:{id:`org_acceptance`,slug:`acceptance`},surface:{id:`surface_acceptance`,slug:`web`,sourceProvider:`github`,destinationProvider:`cli`,environmentFiltering:!0,sdkConfig:null}},{headers:responseHeaders}):HttpResponse.json(url.pathname.includes(`config`)?{plugins:{errors:!0,logs:!0,device:!0,pageEvents:!0,rageClick:!0,replay:!0},...options.sdkConfig}:{},{headers:responseHeaders})}catch(error){return new HttpResponse(new TextEncoder().encode(String(error)),{status:500,headers:responseHeaders})}}async function handle(request,response){try{let headers=new Headers;for(let[key,value]of Object.entries(request.headers))value!==void 0&&headers.set(key,Array.isArray(value)?value.join(`, `):value);let init={method:request.method??`GET`,headers},resolved=await getResponse([http.all(`*`,async({request:intercepted})=>resolveResponse(intercepted,request))],new Request(new URL(request.url??`/`,`http://localhost`),init));if(!resolved)throw Error(`Missing telemetry receiver response`);for(let[key,value]of resolved.headers)response.setHeader(key,value);response.writeHead(resolved.status).end(Buffer.from(await resolved.arrayBuffer()))}catch(error){response.destroy(error instanceof Error?error:Error(String(error)))}}let server=createServer((request,response)=>{handle(request,response)});server.listen(0,`127.0.0.1`),await once(server,`listening`);let address=server.address();if(!(address instanceof Object))throw TypeError(`Receiver did not bind a TCP port`);return{url:`http://127.0.0.1:${address.port}`,requests,serve(directory){assets=directory},forward(handler){upstream=handler},reset(){requests.length=0,failures.clear(),stalls.clear()},fail(path,status,attempts=1,body=`{}`){failures.set(path,{status,remaining:attempts,body})},stall(path,ms){stalls.set(path,ms)},async close(){server.closeAllConnections(),await new Promise((resolve,reject)=>{server.close(error=>{error?reject(error):resolve()})})}}}export{receiver};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/test-utils",
3
- "version": "10.1.0",
3
+ "version": "10.1.2",
4
4
  "description": "Shared test utilities used across Interfere packages.",
5
5
  "homepage": "https://interfere.com",
6
6
  "bugs": "mailto:support@interfere.com",
@@ -39,7 +39,7 @@
39
39
  "typecheck": "bun check"
40
40
  },
41
41
  "dependencies": {
42
- "@interfere/helpers": "^9.3.2",
42
+ "@interfere/helpers": "^9.3.3",
43
43
  "msw": "2.15.0",
44
44
  "zod": "4.6.7"
45
45
  },