Beta-tester safety: preview caps on the level override, and close a race in stop()
Two things needed before testers get level overrides. 1. The override decided what a position could SPEND as well as what it could SEE. A tester at Scintilla overridden to Corona would have had 150,000 real network impressions instead of 2,500, and a 200,000-impression grant pool to hand to real members — real inventory, spent for real. Ad allowances and split-test launches are now metered on trueLevel, and grant pools are capped to small preview amounts (5,000 impressions / 25 copy / 2 pages / 5 ad batches) when a position is overridden: enough to exercise the whole flow, trivial to lose. 2. suite-traffic.stop() read the ledger, made TWO network round trips to the ad network, then wrote back the object it had read seconds earlier — silently discarding any campaign another member launched in that window, with the impressions already spent on the network. It now re-reads after the awaits and mutates the fresh copy. Worth recording that this was the ONLY such race: Node's single thread makes a synchronous read-modify-write atomic, so the meter, grants and split ledgers were never at risk. The danger was only ever the await gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -692,6 +692,12 @@ async function handleApi(req,res,pathname){
|
||||
return {d:dd,inOrg,beta,allowed};
|
||||
}
|
||||
|
||||
// A level override decides what a position can SEE. It must never decide what
|
||||
// a position can SPEND: ad impressions and grant pools are real inventory on
|
||||
// a real network. So anything that costs something is metered on trueLevel.
|
||||
function spendLevel(e){ return (e.d && e.d.trueLevel != null) ? e.d.trueLevel : (e.d ? e.d.level : 1); }
|
||||
function isPreview(e){ return !!(e.d && e.d.levelOverridden); }
|
||||
|
||||
// ── Page Builder ─────────────────────────────────────────────────────────
|
||||
// Level 6 extra pages: /p/<id>/<slug>. Falls back to the member's main page
|
||||
// rather than 404ing, for the same reason /p/<id> falls back to /join/<id>.
|
||||
@@ -817,7 +823,7 @@ async function handleApi(req,res,pathname){
|
||||
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup.',code:500}));
|
||||
if(e.error)return json(res,e.code||500,{error:e.error});
|
||||
if(!e.inOrg||!e.allowed)return json(res,403,{error:'Not available for this position yet.'});
|
||||
const st=suiteTraffic.status(e.d.id,e.d.level);
|
||||
const st=suiteTraffic.status(e.d.id,spendLevel(e));
|
||||
let live=[];
|
||||
try{ live=await suiteTraffic.stats(st.campaigns.map(c=>c.adId)); }catch(err){}
|
||||
// Tell the UI whether a personal page actually exists, so it can't be
|
||||
@@ -836,14 +842,14 @@ async function handleApi(req,res,pathname){
|
||||
try{
|
||||
const pgRec=suitePages.load(e.d.id);
|
||||
const entry=await suiteTraffic.launch({
|
||||
id:e.d.id, level:e.d.level, size:b.size, creative:b.creative,
|
||||
id:e.d.id, level:spendLevel(e), size:b.size, creative:b.creative,
|
||||
kind:b.kind==='text'?'text':'banner',
|
||||
subject:b.subject, lines:b.lines,
|
||||
impressions:b.impressions, target:b.target, angle:b.angle,
|
||||
hasPage:!!(pgRec&&pgRec.copy),
|
||||
name:String(b.name||'').slice(0,60)
|
||||
});
|
||||
return json(res,200,{campaign:entry,status:suiteTraffic.status(e.d.id,e.d.level)});
|
||||
return json(res,200,{campaign:entry,status:suiteTraffic.status(e.d.id,spendLevel(e))});
|
||||
}catch(err){ return json(res,400,{error:String(err.message||err)}); }
|
||||
}
|
||||
|
||||
@@ -890,7 +896,7 @@ async function handleApi(req,res,pathname){
|
||||
let r=[]; try{ r=await suiteSplit.results(e.d.id); }catch(err){}
|
||||
const pg=suitePages.load(e.d.id);
|
||||
return json(res,200,{tests:r,level:e.d.level,id:e.d.id,
|
||||
traffic:suiteTraffic.status(e.d.id,e.d.level),hasPage:!!(pg&&pg.copy),
|
||||
traffic:suiteTraffic.status(e.d.id,spendLevel(e)),hasPage:!!(pg&&pg.copy),
|
||||
minPerArm:suiteSplit.MIN_PER_ARM,maxArms:suiteSplit.MAX_ARMS,
|
||||
textAngles:suiteTextAds.angles(),
|
||||
textMeter:suiteMeter.check(e.d.id,e.d.level,'textad')});
|
||||
@@ -903,9 +909,9 @@ async function handleApi(req,res,pathname){
|
||||
}
|
||||
const pg=suitePages.load(e.d.id);
|
||||
try{
|
||||
const t=await suiteSplit.launch({id:e.d.id,level:e.d.level,arms:b.arms,
|
||||
const t=await suiteSplit.launch({id:e.d.id,level:spendLevel(e),arms:b.arms,
|
||||
impressions:b.impressions,target:b.target,angle:b.angle,hasPage:!!(pg&&pg.copy)});
|
||||
return json(res,200,{test:t,traffic:suiteTraffic.status(e.d.id,e.d.level)});
|
||||
return json(res,200,{test:t,traffic:suiteTraffic.status(e.d.id,spendLevel(e))});
|
||||
}catch(err){ return json(res,400,{error:String(err.message||err)}); }
|
||||
}
|
||||
}
|
||||
@@ -1031,7 +1037,7 @@ async function handleApi(req,res,pathname){
|
||||
if(req.method==='GET'){
|
||||
// Recipients see what they were given even below level 7 - that is the
|
||||
// half of the feature that makes being helped visible.
|
||||
return json(res,200,{status:suiteGrants.status(e.d.id,e.d.level),tools:suiteGrants.tools(),
|
||||
return json(res,200,{status:suiteGrants.status(e.d.id,e.d.level,isPreview(e)),tools:suiteGrants.tools(),
|
||||
minLevel:suiteGrants.MIN_LEVEL,level:e.d.level,id:e.d.id});
|
||||
}
|
||||
if(req.method==='POST'){
|
||||
@@ -1044,8 +1050,8 @@ async function handleApi(req,res,pathname){
|
||||
try{ inTeam=chain.isInTeam(to,Number(e.d.id)); }catch(err){}
|
||||
if(!inTeam)return json(res,400,{error:'Position #'+to+' is not in your organisation. You can only grant to people below you.'});
|
||||
try{
|
||||
const rec=suiteGrants.grant({by:e.d.id,to:to,tool:String(b.tool||''),n:b.n,level:e.d.level});
|
||||
return json(res,200,{granted:rec,status:suiteGrants.status(e.d.id,e.d.level)});
|
||||
const rec=suiteGrants.grant({by:e.d.id,to:to,tool:String(b.tool||''),n:b.n,level:e.d.level,preview:isPreview(e)});
|
||||
return json(res,200,{granted:rec,status:suiteGrants.status(e.d.id,e.d.level,isPreview(e))});
|
||||
}catch(err){ return json(res,400,{error:String(err.message||err)}); }
|
||||
}
|
||||
}
|
||||
@@ -1101,7 +1107,7 @@ async function handleApi(req,res,pathname){
|
||||
const b=await bodyJson(req)||{};
|
||||
try{
|
||||
const r=await suiteTraffic.stop(e.d.id,Number(b.ad_id));
|
||||
return json(res,200,{stopped:r,status:suiteTraffic.status(e.d.id,e.d.level)});
|
||||
return json(res,200,{stopped:r,status:suiteTraffic.status(e.d.id,spendLevel(e))});
|
||||
}catch(err){ return json(res,400,{error:String(err.message||err)}); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user