'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const vm = require('node:vm'); const create = require('../lib/capture-funnel'); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-funnel-test-')); const file = path.join(dir, 'events.jsonl'); const funnel = create(file); const today = Date.parse('2026-10-16T02:00:00Z'), yesterday = today - 86400000; const stages = ['started','front','right','left','submitted','response','report']; const body = (id, n) => ({ id: id.repeat(32), events: stages.slice(0,n).map((stage,i)=>({stage,ms:i*1000})) }); assert(funnel.ingest(body('a',7), yesterday)); assert(funnel.ingest(body('a',2), today)); // late old snapshot cannot regress assert(funnel.ingest(body('a',7), today)); // retries do not inflate assert(funnel.ingest(body('b',6), today)); assert(funnel.ingest({...body('c',1),events:[{stage:'started',ms:0},{stage:'failed',ms:500}]},today)); assert(funnel.ingest({...body('d',1),events:[{stage:'started',ms:0},{stage:'canceled',ms:600}]},today)); assert(funnel.ingest({...body('e',1),events:[{stage:'started',ms:0},{stage:'interrupted',ms:700}]},today)); assert.equal(funnel.ingest({id:'x',events:[]}),false); assert.equal(funnel.ingest({id:'f'.repeat(32),events:[{stage:'started',ms:0},{stage:'report',ms:20}]}),false); assert.equal(funnel.ingest({id:'f'.repeat(32),events:[{stage:'started',ms:0},{stage:'front',ms:-1}]}),false); const stats=funnel.stats(today); assert.equal(stats.total.counts.started,5); assert.equal(stats.total.counts.report,1); assert.equal(stats.total.rate,20);assert.equal(stats.total.avg_ms,6000); assert.equal(stats.today.counts.started,4);assert.equal(stats.today.counts.report,0); assert.equal(stats.total.response_without_report,1);assert.equal(stats.total.unfinished,2); assert.deepEqual(create(file).stats(today),stats); // durable after service restart const rows=fs.readFileSync(file,'utf8').trim().split('\n').map(JSON.parse); assert.equal(rows.length,19);assert.deepEqual(Object.keys(rows[0]),['id','stage','ms','at']); // Run actual browser telemetry helpers in isolation: no real network, no paid API. const source=fs.readFileSync(path.join(__dirname,'../js/app.js'),'utf8'); const helper=source.slice(source.indexOf(' var captureJourney ='), source.indexOf(' /* camera-outline-pure-begin */')); const calls=[];const ctx={ IS_MOCK:false,Uint8Array,Array,Date,Math,JSON, window:{crypto:require('node:crypto').webcrypto},fetch(url,opts){calls.push({url,opts});return Promise.resolve({ok:true});} }; vm.createContext(ctx);vm.runInContext(helper,ctx); ctx.beginCaptureJourney(); stages.slice(1).forEach(s=>ctx.trackCapture(s)); ctx.trackCapture('report');ctx.trackCapture('canceled'); assert.equal(calls.length,7);assert.equal(calls[6].url,'api/capture-funnel'); assert.equal(calls[6].opts.keepalive,true);assert(funnel.ingest(JSON.parse(calls[6].opts.body),today)); ctx.IS_MOCK=true;ctx.beginCaptureJourney();ctx.trackCapture('started');assert.equal(calls.length,7); ctx.IS_MOCK=false;ctx.beginCaptureJourney();ctx.trackCapture('canceled');ctx.trackCapture('front');assert.equal(calls.length,9); ctx.fetch=()=>{throw Error('unavailable');};assert.doesNotThrow(()=>ctx.beginCaptureJourney()); assert(source.indexOf("trackCapture('report')")>source.indexOf("showScreen('s-report', 'replace')")); console.log('PASS: deduplicated durable anonymous funnel, cumulative/out-of-order delivery, Beijing cohorts, terminal/missing states, mock exclusion, nonblocking browser telemetry');