code wiki / _hdl_build / nx_adnet_view.nx
nx_adnet_view.nx source
↩ module page · 85 lines · 4732 B
1// nx_adnet_view.nx -- LIB: MRC-compliant VIEWABLE-impression measurement for the sovereign ad slot.
2// THE DEFECT THIS CLOSES (debt 1785512185): the served-impression counter fires when the slot HTML is
3// INJECTED, before the browser has laid anything out. With a lazy-loaded creative that is frequently never
4// fetched at all, an advertiser is billed for pixels no human ever saw. The 2026 MRC display standard is
5// 50 percent of the creative's pixels in view for 1 CONTINUOUS second; a number weaker than that is not
6// sellable to a paying client, and billing on it is the credibility rock for the whole ad network.
7//
8// PRIVACY BY CONSTRUCTION, UNCHANGED: the beacon carries the AD id and NOTHING ELSE -- no cookie, no
9// visitor id, no dwell curve, no referrer, no timestamp from the client. The schema HAS NO VISITOR FIELD,
10// exactly like nx_adnet_selfserve. First-party only: same-origin sendBeacon and an inline script (the live
11// CSP is script-src 'self' 'unsafe-inline' and connect-src 'self'), zero third-party JS.
12//
13// FAIL-CLOSED: an id that is not a well-formed inventory id is DROPPED, never journalled -- a fabricated
14// or replayed id must never be able to manufacture a billable viewable impression.
15// license_tier: ORIGINAL
16import "nx_syscalls.nx"
17import "_hdl_build/nx_adnet_slot.nx"
18
19// MRC display thresholds -- the STANDARD, not taste. Weakening either makes the number unsellable, so
20// they are named constants the gate asserts against rather than digits buried in a script string.
21const AVIEW_RATIO_PCT: i64 = 50
22const AVIEW_DWELL_MS: i64 = 1000
23
24// decimal emit (no dynamic formatting in the emitted JS -- the thresholds come from the consts above,
25// so the script and the gate can never drift apart).
26func aview_putdec(out: *u8, off: i64, v: i64) -> i64 {
27 if v == 0 { out[off] = 48 as u8; return off + 1 }
28 let tmp: *u8 = sys_mmap(32)
29 var n: i64 = 0
30 var x: i64 = v
31 while x > 0 { tmp[n] = ((x - (x / 10) * 10) + 48) as u8; n = n + 1; x = x / 10 }
32 var o: i64 = off
33 var i: i64 = n - 1
34 while i >= 0 { out[o] = tmp[i]; o = o + 1; i = i - 1 }
35 return o
36}
37
38// Emit the first-party viewability observer for ONE ad id. Returns bytes written, 0 = refused.
39// The timer is CANCELLED when the creative leaves the threshold, which is what makes the second
40// CONTINUOUS rather than cumulative -- cumulative dwell is the usual way this measurement gets faked.
41// Fires at most once per page (o.disconnect + a sent latch), so a scroll-by cannot inflate the count.
42func aview_script(out: *u8, cap: i64, ad_id: *u8) -> i64 {
43 if cap < 1024 { return 0 }
44 if aslot_id_ok(ad_id) == 0 { return 0 }
45 var o: i64 = 0
46 o = ad_cat(out, o, "<script>(function(){var a=document.querySelector('.nx-ad-slot img');if(!a||!window.IntersectionObserver)return;var t=null,s=false;var o=new IntersectionObserver(function(en){var r=en[0];if(r.isIntersecting&&r.intersectionRatio*100>=" as *u8)
47 o = aview_putdec(out, o, AVIEW_RATIO_PCT)
48 o = ad_cat(out, o, "){if(t===null){t=setTimeout(function(){if(s)return;s=true;o.disconnect();try{navigator.sendBeacon('/ad/view/" as *u8)
49 o = ad_cat(out, o, ad_id)
50 o = ad_cat(out, o, "','')}catch(e){}}," as *u8)
51 o = aview_putdec(out, o, AVIEW_DWELL_MS)
52 o = ad_cat(out, o, ")}}else{if(t!==null){clearTimeout(t);t=null}}},{threshold:[0,0.5,1]});o.observe(a)})()</script>" as *u8)
53 out[o] = 0 as u8
54 return o
55}
56
57// Is this request path a viewable beacon? /ad/view/<id>
58func aview_is_path(path: *u8, pn: i64) -> i64 {
59 if pn < 10 { return 0 }
60 return aslot_starts(path, "/ad/view/" as *u8)
61}
62
63// Handle /ad/view/<id>: emit a 204 (no body -- a beacon has no reply to render) and set idout to the
64// VALIDATED id so the caller journals exactly one viewable impression. Returns response bytes, 0 = not
65// a beacon path. idout stays empty on a malformed id: the 204 is still emitted so a probe cannot use the
66// status code to enumerate which ids are live, but NOTHING is journalled.
67func aview_resp(path: *u8, pn: i64, out: *u8, cap: i64, idout: *u8) -> i64 {
68 idout[0] = 0 as u8
69 if aview_is_path(path, pn) == 0 { return 0 }
70 if cap < 256 { return 0 }
71 let id: *u8 = sys_mmap(128)
72 var i: i64 = 9
73 var j: i64 = 0
74 while i < pn { if j < 100 { id[j] = path[i]; j = j + 1 } i = i + 1 }
75 id[j] = 0 as u8
76 if aslot_id_ok(id) == 1 {
77 var k: i64 = 0
78 while id[k] != (0 as u8) { idout[k] = id[k]; k = k + 1 }
79 idout[k] = 0 as u8
80 }
81 var o: i64 = 0
82 o = ad_cat(out, o, "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: keep-alive\r\nKeep-Alive: timeout=65\r\nCache-Control: no-store\r\nX-Robots-Tag: noindex\r\n\r\n" as *u8)
83 out[o] = 0 as u8
84 return o
85}