@iann29/rastro 0.1.0-alpha.5 → 0.1.0-alpha.7

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.
Files changed (125) hide show
  1. package/README.md +82 -29
  2. package/agent/integration.md +17 -15
  3. package/agent/manifest.json +7 -5
  4. package/agent/manifest.schema.json +8 -5
  5. package/dist/client/federation.d.ts +34 -8
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +17 -3
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/index.d.ts +418 -21
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +191 -41
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +2 -0
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/api.js.map +1 -1
  16. package/dist/component/_generated/component.d.ts +63 -7
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/affiliates.d.ts.map +1 -1
  19. package/dist/component/affiliates.js +6 -2
  20. package/dist/component/affiliates.js.map +1 -1
  21. package/dist/component/cardinality.d.ts.map +1 -1
  22. package/dist/component/cardinality.js.map +1 -1
  23. package/dist/component/constants.d.ts +7 -1
  24. package/dist/component/constants.d.ts.map +1 -1
  25. package/dist/component/constants.js +13 -1
  26. package/dist/component/constants.js.map +1 -1
  27. package/dist/component/diagnostics.d.ts.map +1 -1
  28. package/dist/component/diagnostics.js.map +1 -1
  29. package/dist/component/eventStore.d.ts +2 -1
  30. package/dist/component/eventStore.d.ts.map +1 -1
  31. package/dist/component/eventStore.js +13 -10
  32. package/dist/component/eventStore.js.map +1 -1
  33. package/dist/component/funnels.d.ts.map +1 -1
  34. package/dist/component/funnels.js +5 -3
  35. package/dist/component/funnels.js.map +1 -1
  36. package/dist/component/geo.d.ts.map +1 -1
  37. package/dist/component/geo.js +3 -2
  38. package/dist/component/geo.js.map +1 -1
  39. package/dist/component/goals.d.ts.map +1 -1
  40. package/dist/component/goals.js +8 -5
  41. package/dist/component/goals.js.map +1 -1
  42. package/dist/component/guards.d.ts.map +1 -1
  43. package/dist/component/guards.js.map +1 -1
  44. package/dist/component/http.d.ts.map +1 -1
  45. package/dist/component/http.js +51 -17
  46. package/dist/component/http.js.map +1 -1
  47. package/dist/component/ingest.d.ts +2 -1
  48. package/dist/component/ingest.d.ts.map +1 -1
  49. package/dist/component/ingest.js +143 -23
  50. package/dist/component/ingest.js.map +1 -1
  51. package/dist/component/live.d.ts.map +1 -1
  52. package/dist/component/live.js +1 -3
  53. package/dist/component/live.js.map +1 -1
  54. package/dist/component/reports.d.ts +60 -3
  55. package/dist/component/reports.d.ts.map +1 -1
  56. package/dist/component/reports.js +215 -49
  57. package/dist/component/reports.js.map +1 -1
  58. package/dist/component/retention.d.ts +4 -4
  59. package/dist/component/retention.d.ts.map +1 -1
  60. package/dist/component/retention.js +54 -22
  61. package/dist/component/retention.js.map +1 -1
  62. package/dist/component/sanitize.d.ts +11 -0
  63. package/dist/component/sanitize.d.ts.map +1 -1
  64. package/dist/component/sanitize.js +58 -11
  65. package/dist/component/sanitize.js.map +1 -1
  66. package/dist/component/schema.d.ts +47 -5
  67. package/dist/component/schema.js +21 -1
  68. package/dist/component/schema.js.map +1 -1
  69. package/dist/component/sites.d.ts.map +1 -1
  70. package/dist/component/sites.js +6 -6
  71. package/dist/component/sites.js.map +1 -1
  72. package/dist/component/validators.d.ts +14 -9
  73. package/dist/component/validators.d.ts.map +1 -1
  74. package/dist/component/validators.js +5 -1
  75. package/dist/component/validators.js.map +1 -1
  76. package/dist/component/visitors.d.ts.map +1 -1
  77. package/dist/component/visitors.js.map +1 -1
  78. package/dist/component/vitals.d.ts +41 -0
  79. package/dist/component/vitals.d.ts.map +1 -0
  80. package/dist/component/vitals.js +115 -0
  81. package/dist/component/vitals.js.map +1 -0
  82. package/dist/react/index.d.ts.map +1 -1
  83. package/dist/react/index.js.map +1 -1
  84. package/dist/tracker/generated.d.ts +8 -4
  85. package/dist/tracker/generated.d.ts.map +1 -1
  86. package/dist/tracker/generated.js +8 -4
  87. package/dist/tracker/generated.js.map +1 -1
  88. package/dist/tracker/tracker.d.ts.map +1 -1
  89. package/dist/tracker/tracker.js +3 -1
  90. package/dist/tracker/tracker.js.map +1 -1
  91. package/dist/tracker/vitals.d.ts +10 -0
  92. package/dist/tracker/vitals.d.ts.map +1 -0
  93. package/dist/tracker/vitals.js +140 -0
  94. package/dist/tracker/vitals.js.map +1 -0
  95. package/dist/tracker.min.js +1 -1
  96. package/dist/vitals.min.js +1 -0
  97. package/docs/benchmarks/2026-08-20-realistic.md +71 -71
  98. package/docs/benchmarks/2026-08-21-formal-certification.md +50 -30
  99. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  100. package/docs/upgrading.md +46 -8
  101. package/llms.txt +5 -1
  102. package/package.json +17 -3
  103. package/scripts/benchmark-ingest.mjs +101 -48
  104. package/src/component/_generated/api.ts +2 -0
  105. package/src/component/_generated/component.ts +69 -4
  106. package/src/component/affiliates.ts +20 -5
  107. package/src/component/cardinality.ts +8 -7
  108. package/src/component/constants.ts +13 -1
  109. package/src/component/diagnostics.ts +3 -2
  110. package/src/component/eventStore.ts +53 -46
  111. package/src/component/funnels.ts +19 -16
  112. package/src/component/geo.ts +16 -14
  113. package/src/component/goals.ts +29 -21
  114. package/src/component/guards.ts +3 -1
  115. package/src/component/http.ts +93 -54
  116. package/src/component/ingest.ts +320 -127
  117. package/src/component/live.ts +5 -4
  118. package/src/component/reports.ts +563 -219
  119. package/src/component/retention.ts +221 -98
  120. package/src/component/sanitize.ts +99 -29
  121. package/src/component/schema.ts +31 -5
  122. package/src/component/sites.ts +22 -11
  123. package/src/component/validators.ts +16 -7
  124. package/src/component/visitors.ts +16 -5
  125. package/src/component/vitals.ts +146 -0
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ // The opt-in Web Vitals module. It shares the main tracker's session identity
3
+ // through sessionStorage but never depends on it: either script works alone.
4
+ // Values are integers — milliseconds, except CLS which ships scaled by 1000.
5
+ (() => {
6
+ const script = document.currentScript;
7
+ const siteId = script?.dataset.site;
8
+ const endpoint = script?.dataset.endpoint ||
9
+ (script?.src && new URL("events", script.src).href);
10
+ if (!siteId || !endpoint)
11
+ return;
12
+ const createId = () => crypto.randomUUID?.() || Math.random() + "";
13
+ let sessionId;
14
+ try {
15
+ sessionId = sessionStorage._r || createId();
16
+ sessionStorage._r = sessionId;
17
+ }
18
+ catch {
19
+ sessionId = createId();
20
+ }
21
+ const visitorId = script.dataset.visitor || sessionId;
22
+ // Metrics describe the loaded document, so they keep the landing path even
23
+ // when a SPA rewrites the URL later.
24
+ const path = location.pathname.slice(0, 256);
25
+ const values = {};
26
+ const observers = [];
27
+ const observe = (type, handler, extra) => {
28
+ try {
29
+ const observer = new PerformanceObserver((list) => handler(list.getEntries()));
30
+ observer.observe({
31
+ type,
32
+ buffered: true,
33
+ ...extra,
34
+ });
35
+ observers.push({ observer, handler });
36
+ }
37
+ catch {
38
+ // The metric simply goes unreported where the entry type is missing.
39
+ }
40
+ };
41
+ const navigation = performance.getEntriesByType?.("navigation")[0];
42
+ if (navigation && navigation.responseStart > 0) {
43
+ values.TTFB = navigation.responseStart;
44
+ }
45
+ observe("paint", (entries) => {
46
+ for (const entry of entries) {
47
+ if (entry.name === "first-contentful-paint")
48
+ values.FCP = entry.startTime;
49
+ }
50
+ });
51
+ observe("largest-contentful-paint", (entries) => {
52
+ const last = entries[entries.length - 1];
53
+ if (last)
54
+ values.LCP = last.startTime;
55
+ });
56
+ // CLS session windows: shifts joined by <1s gaps within a 5s span; the worst
57
+ // window is the score.
58
+ let clsWindow = 0;
59
+ let clsFirst = 0;
60
+ let clsLast = 0;
61
+ observe("layout-shift", (entries) => {
62
+ for (const entry of entries) {
63
+ if (entry.hadRecentInput)
64
+ continue;
65
+ if (clsWindow &&
66
+ entry.startTime - clsLast < 1_000 &&
67
+ entry.startTime - clsFirst < 5_000) {
68
+ clsWindow += entry.value;
69
+ }
70
+ else {
71
+ clsWindow = entry.value;
72
+ clsFirst = entry.startTime;
73
+ }
74
+ clsLast = entry.startTime;
75
+ values.CLS = Math.max(values.CLS || 0, clsWindow * 1_000);
76
+ }
77
+ });
78
+ // INP keeps the worst durations and reports the (interactions / 50)th
79
+ // longest — the same estimate the web-vitals library uses.
80
+ const durations = [];
81
+ const interactionIds = new Set();
82
+ const interactions = (entries) => {
83
+ for (const entry of entries) {
84
+ if (!entry.interactionId)
85
+ continue;
86
+ interactionIds.add(entry.interactionId);
87
+ durations.push(entry.duration);
88
+ }
89
+ if (!durations.length)
90
+ return;
91
+ durations.sort((left, right) => right - left);
92
+ durations.length = Math.min(durations.length, 10);
93
+ const count = performance.interactionCount ||
94
+ interactionIds.size;
95
+ values.INP =
96
+ durations[Math.min(durations.length - 1, Math.floor(count / 50))];
97
+ };
98
+ observe("event", interactions, { durationThreshold: 40 });
99
+ observe("first-input", interactions);
100
+ let sent = false;
101
+ const send = () => {
102
+ if (sent)
103
+ return;
104
+ for (const { observer, handler } of observers) {
105
+ handler(observer.takeRecords());
106
+ observer.disconnect();
107
+ }
108
+ const timestamp = Date.now();
109
+ const events = Object.keys(values).map((name, index) => ({
110
+ eventId: `${sessionId}.${timestamp.toString(36)}.v${index}`,
111
+ sessionId,
112
+ visitorId,
113
+ type: "vital",
114
+ name,
115
+ value: Math.round(values[name]),
116
+ path,
117
+ timestamp,
118
+ sequence: index,
119
+ }));
120
+ if (!events.length)
121
+ return;
122
+ sent = true;
123
+ // sentAt lets the server re-anchor a clock that is minutes off.
124
+ const body = new Blob([
125
+ JSON.stringify({ siteId, events, sentAt: timestamp }),
126
+ ]);
127
+ const beacon = navigator.sendBeacon?.bind(navigator);
128
+ if (!beacon || !beacon(endpoint, body)) {
129
+ void fetch(endpoint, { method: "POST", body, keepalive: true }).catch(() => {
130
+ // Best effort by design: measurement must never break the host page.
131
+ });
132
+ }
133
+ };
134
+ document.addEventListener("visibilitychange", () => {
135
+ if (document.visibilityState === "hidden")
136
+ send();
137
+ });
138
+ addEventListener("pagehide", send);
139
+ })();
140
+ //# sourceMappingURL=vitals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitals.js","sourceRoot":"","sources":["../../src/tracker/vitals.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- (()=>{const e=document.currentScript,t=e?.dataset.site,s=e?.dataset.endpoint||e?.src&&new URL("events",e.src).href;if(!t||!s)return;const o=()=>crypto.randomUUID?.()||Math.random()+"",n=new URLSearchParams(location.search),r=n.get("ref")?.slice(0,64);let i,a,c;for(const e of["source","medium","campaign","term","content"]){const t=n.get("utm_"+e)?.slice(0,64);t&&((i??={})["utm_"+e]=t)}try{a=sessionStorage._r||o(),sessionStorage._r=a,c=r||sessionStorage._a,c&&(sessionStorage._a=c),i||=sessionStorage._u&&JSON.parse(sessionStorage._u),i&&(sessionStorage._u=JSON.stringify(i))}catch{a=o(),c=r}const d=e.dataset.visitor||a;let l,p=0;const g=[],u=document.referrer?new URL(document.referrer).origin:void 0,h=e=>{const o=g.splice(0,50);if(!o.length)return;const n=()=>new Blob([JSON.stringify({siteId:t,events:o})]);let r=n();for(;r.size>64e3&&o.length>1;)g.unshift(o.pop()),r=n();e&&navigator.sendBeacon(s,r)||fetch(s,{method:"POST",body:r,keepalive:1}).catch(()=>{}),g.length&&h(e)},m=(e,t,s)=>{const o=Date.now();g.push({eventId:`${a}.${o.toString(36)}.${p}`,sessionId:a,visitorId:d,type:e,name:t,path:location.pathname.slice(0,256),referrer:u,timestamp:o,sequence:p++,affiliateSlug:c,...s}),clearTimeout(l),l=setTimeout(h,1200)};let f;const v=()=>{const e=location.pathname;e!==f&&(f=e,m("pageview",void 0,i&&{properties:i}))};window.rastro=(e,t,s)=>{if("context"===e)return{sessionId:a,visitorId:d};"event"===e&&m("custom",`${t||"event"}`.slice(0,80),s&&JSON.stringify(s).length>2e4?void 0:{properties:s})},document.addEventListener("click",e=>{const t=e.target?.closest("a,button,[data-rastro-event]");if(!t)return;const s=t.dataset.rastroEvent,o=t.href,n=o&&new URL(o),r=/^https?:$/.test(n?.protocol),i=r&&n.origin!==location.origin;m(s?"custom":i?"outbound":"click",s,{target:(t.dataset.rastroLabel||t.innerText||t.tagName).slice(0,64),href:r?((i?n.origin:"")+n.pathname).slice(0,256):void 0})},1);const S=e=>{const t=history[e];history[e]=(...e)=>{t.apply(history,e),v()}};S("pushState"),S("replaceState"),addEventListener("popstate",v),addEventListener("pagehide",()=>h(1)),setInterval(()=>{m("heartbeat"),h()},2e4),v()})();
1
+ (()=>{const e=document.currentScript,t=e?.dataset.site,s=e?.dataset.endpoint||e?.src&&new URL("events",e.src).href;if(!t||!s)return;const o=()=>crypto.randomUUID?.()||Math.random()+"",n=new URLSearchParams(location.search),r=n.get("ref")?.slice(0,64);let a,i,c;for(const e of["source","medium","campaign","term","content"]){const t=n.get("utm_"+e)?.slice(0,64);t&&((a??={})["utm_"+e]=t)}try{i=sessionStorage._r||o(),sessionStorage._r=i,c=r||sessionStorage._a,c&&(sessionStorage._a=c),a||=sessionStorage._u&&JSON.parse(sessionStorage._u),a&&(sessionStorage._u=JSON.stringify(a))}catch{i=o(),c=r}const d=e.dataset.visitor||i;let l,p=0;const g=[],u=document.referrer?new URL(document.referrer).origin:void 0,h=e=>{const o=g.splice(0,50);if(!o.length)return;const n=()=>new Blob([JSON.stringify({siteId:t,events:o,sentAt:Date.now()})]);let r=n();for(;r.size>64e3&&o.length>1;)g.unshift(o.pop()),r=n();e&&navigator.sendBeacon(s,r)||fetch(s,{method:"POST",body:r,keepalive:1}).catch(()=>{}),g.length&&h(e)},m=(e,t,s)=>{const o=Date.now();g.push({eventId:`${i}.${o.toString(36)}.${p}`,sessionId:i,visitorId:d,type:e,name:t,path:location.pathname.slice(0,256),referrer:u,timestamp:o,sequence:p++,affiliateSlug:c,...s}),clearTimeout(l),l=setTimeout(h,1200)};let f;const v=()=>{const e=location.pathname;e!==f&&(f=e,m("pageview",void 0,a&&{properties:a}))};window.rastro=(e,t,s)=>{if("context"===e)return{sessionId:i,visitorId:d};"event"===e&&m("custom",`${t||"event"}`.slice(0,80),s&&JSON.stringify(s).length>2e4?void 0:{properties:s})},document.addEventListener("click",e=>{const t=e.target?.closest("a,button,[data-rastro-event]");if(!t)return;const s=t.dataset.rastroEvent,o=t.href,n=o&&new URL(o),r=/^https?:$/.test(n?.protocol),a=r&&n.origin!==location.origin;m(s?"custom":a?"outbound":"click",s,{target:(t.dataset.rastroLabel||t.innerText||t.tagName).slice(0,64),href:r?((a?n.origin:"")+n.pathname).slice(0,256):void 0})},1);const S=e=>{const t=history[e];history[e]=(...e)=>{t.apply(history,e),v()}};S("pushState"),S("replaceState"),addEventListener("popstate",v),addEventListener("pagehide",()=>h(1)),setInterval(()=>{m("heartbeat"),h()},2e4),v()})();
@@ -0,0 +1 @@
1
+ (()=>{const t=document.currentScript,e=t?.dataset.site,n=t?.dataset.endpoint||t?.src&&new URL("events",t.src).href;if(!e||!n)return;const r=()=>crypto.randomUUID?.()||Math.random()+"";let a;try{a=sessionStorage._r||r(),sessionStorage._r=a}catch{a=r()}const s=t.dataset.visitor||a,o=location.pathname.slice(0,256),i={},c=[],d=(t,e,n)=>{try{const r=new PerformanceObserver(t=>e(t.getEntries()));r.observe({type:t,buffered:1,...n}),c.push({observer:r,handler:e})}catch{}},h=performance.getEntriesByType?.("navigation")[0];h&&h.responseStart>0&&(i.TTFB=h.responseStart),d("paint",t=>{for(const e of t)"first-contentful-paint"===e.name&&(i.FCP=e.startTime)}),d("largest-contentful-paint",t=>{const e=t[t.length-1];e&&(i.LCP=e.startTime)});let l=0,f=0,m=0;d("layout-shift",t=>{for(const e of t)e.hadRecentInput||(l&&e.startTime-m<1e3&&e.startTime-f<5e3?l+=e.value:(l=e.value,f=e.startTime),m=e.startTime,i.CLS=Math.max(i.CLS||0,1e3*l))});const u=[],p=new Set,v=t=>{for(const e of t)e.interactionId&&(p.add(e.interactionId),u.push(e.duration));if(!u.length)return;u.sort((t,e)=>e-t),u.length=Math.min(u.length,10);const e=performance.interactionCount||p.size;i.INP=u[Math.min(u.length-1,Math.floor(e/50))]};d("event",v,{durationThreshold:40}),d("first-input",v);let g=0;const y=()=>{if(g)return;for(const{observer:t,handler:e}of c)e(t.takeRecords()),t.disconnect();const t=Date.now(),r=Object.keys(i).map((e,n)=>({eventId:`${a}.${t.toString(36)}.v${n}`,sessionId:a,visitorId:s,type:"vital",name:e,value:Math.round(i[e]),path:o,timestamp:t,sequence:n}));if(!r.length)return;g=1;const d=new Blob([JSON.stringify({siteId:e,events:r,sentAt:t})]),h=navigator.sendBeacon?.bind(navigator);h&&h(n,d)||fetch(n,{method:"POST",body:d,keepalive:1}).catch(()=>{})};document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&y()}),addEventListener("pagehide",y)})();
@@ -1,13 +1,13 @@
1
1
  # Realistic ingest validation, 2026-08-20
2
2
 
3
3
  This report preserves the performance evidence produced while developing PR
4
- [#8](https://github.com/amageweb/amage-rastro/pull/8), merged as `93a0b4e`.
5
- It is a historical engineering record, not a production SLA.
4
+ [#8](https://github.com/amageweb/amage-rastro/pull/8), merged as `93a0b4e`. It
5
+ is a historical engineering record, not a production SLA.
6
6
 
7
7
  ## Status
8
8
 
9
- - The realistic workload demonstrated the 100 million accepted-events/day
10
- target during a sustained ten-minute soak.
9
+ - The realistic workload demonstrated the 100 million accepted-events/day target
10
+ during a sustained ten-minute soak.
11
11
  - The sustained run predates the current `--certify` contract and did not
12
12
  preserve a run-scoped JSON artifact with aggregate reconciliation.
13
13
  - A later smoke against the final PR code reconciled every expected stored
@@ -22,19 +22,19 @@ certified for 100 million stored events/day.
22
22
 
23
23
  ## Environment
24
24
 
25
- | Item | Value |
26
- | --- | --- |
27
- | Date | 2026-08-20 |
28
- | Platform | Synapse-managed Convex |
29
- | Deployment | DEV canary `fast-finch-7454` |
30
- | Allocation | 4 GiB |
31
- | Site scope | One dedicated benchmark site |
32
- | Profile | `realistic` |
33
- | Concurrency | 75 workers |
34
- | Batch size | 50 events |
35
- | Session pool | 2,048 reused sessions |
36
- | Browser context | 12 geographic locations and 4 user-agent families |
37
- | Event mix | Pageviews, clicks, custom events, outbound events, and heartbeats |
25
+ | Item | Value |
26
+ | --------------- | ----------------------------------------------------------------- |
27
+ | Date | 2026-08-20 |
28
+ | Platform | Synapse-managed Convex |
29
+ | Deployment | DEV canary `fast-finch-7454` |
30
+ | Allocation | 4 GiB |
31
+ | Site scope | One dedicated benchmark site |
32
+ | Profile | `realistic` |
33
+ | Concurrency | 75 workers |
34
+ | Batch size | 50 events |
35
+ | Session pool | 2,048 reused sessions |
36
+ | Browser context | 12 geographic locations and 4 user-agent families |
37
+ | Event mix | Pageviews, clicks, custom events, outbound events, and heartbeats |
38
38
 
39
39
  The deployment name is historical and may be deleted or repointed. Never use
40
40
  this document as deployment configuration.
@@ -44,51 +44,51 @@ this document as deployment configuration.
44
44
  The sustained soak ran after the raw-event batching and five-second live-sweep
45
45
  work in `bbfb9ad`, before the final review fixes later included in `93a0b4e`.
46
46
 
47
- | Metric | Result |
48
- | --- | ---: |
49
- | Target duration | 600 seconds |
50
- | Accepted events | 1,067,650 |
51
- | Failed requests | 0 |
52
- | Rejected events | 0 |
53
- | Accepted throughput | 1,773 events/s |
54
- | Accepted daily-rate estimate | 153.2 million events/day |
55
- | Target daily rate | 100 million events/day |
56
- | Headroom over target | 53.2% |
57
- | Request latency p99 | approximately 5.4 seconds |
47
+ | Metric | Result |
48
+ | ---------------------------- | ------------------------: |
49
+ | Target duration | 600 seconds |
50
+ | Accepted events | 1,067,650 |
51
+ | Failed requests | 0 |
52
+ | Rejected events | 0 |
53
+ | Accepted throughput | 1,773 events/s |
54
+ | Accepted daily-rate estimate | 153.2 million events/day |
55
+ | Target daily rate | 100 million events/day |
56
+ | Headroom over target | 53.2% |
57
+ | Request latency p99 | approximately 5.4 seconds |
58
58
 
59
59
  The target requires approximately 1,157 accepted events/s. The daily figure is
60
60
  an extrapolation from the measured soak, not a full-day load test.
61
61
 
62
62
  This run established sustained accepted-ingress capacity. Its exact heartbeat
63
- count and stored-event delta were not retained, so it must not be presented as
64
- a formally reconciled stored-events certification under the current harness.
63
+ count and stored-event delta were not retained, so it must not be presented as a
64
+ formally reconciled stored-events certification under the current harness.
65
65
 
66
66
  ## Reconciled corroboration
67
67
 
68
68
  A separate two-minute realistic rerun on the same canary used the same
69
69
  concurrency, batch size, and session pool:
70
70
 
71
- | Metric | Result |
72
- | --- | ---: |
73
- | Run label | `realistic-2m-rerun` |
74
- | Run ID | `mt1ydsq5-jjmbjv` |
75
- | Target duration | 120 seconds |
76
- | Measured elapsed time | 122.021 seconds |
77
- | Requests completed | 4,363 |
78
- | Accepted events | 218,150 |
79
- | Accepted heartbeats | 10,775 |
80
- | Expected stored events | 207,375 |
81
- | Aggregate event delta | 207,375 |
82
- | Reconciliation | exact |
83
- | Failed requests | 0 |
84
- | Duplicate events | 0 |
85
- | Rejected events | 0 |
86
- | Accepted throughput | 1,788 events/s |
71
+ | Metric | Result |
72
+ | ---------------------------- | ---------------------: |
73
+ | Run label | `realistic-2m-rerun` |
74
+ | Run ID | `mt1ydsq5-jjmbjv` |
75
+ | Target duration | 120 seconds |
76
+ | Measured elapsed time | 122.021 seconds |
77
+ | Requests completed | 4,363 |
78
+ | Accepted events | 218,150 |
79
+ | Accepted heartbeats | 10,775 |
80
+ | Expected stored events | 207,375 |
81
+ | Aggregate event delta | 207,375 |
82
+ | Reconciliation | exact |
83
+ | Failed requests | 0 |
84
+ | Duplicate events | 0 |
85
+ | Rejected events | 0 |
86
+ | Accepted throughput | 1,788 events/s |
87
87
  | Accepted daily-rate estimate | 154,466,612 events/day |
88
- | Request latency p50 | 2,049 ms |
89
- | Request latency p95 | 2,570 ms |
90
- | Request latency p99 | 3,082 ms |
91
- | Request latency max | 3,366 ms |
88
+ | Request latency p50 | 2,049 ms |
89
+ | Request latency p95 | 2,570 ms |
90
+ | Request latency p99 | 3,082 ms |
91
+ | Request latency max | 3,366 ms |
92
92
 
93
93
  Reconciliation was a site-wide before/after delta. It is reliable only because
94
94
  the benchmark site was kept idle apart from this run.
@@ -99,28 +99,28 @@ After the final correctness review, commit `c43d971` was deployed to the canary.
99
99
  That source was subsequently squash-merged unchanged as `93a0b4e`. A 30-second
100
100
  smoke produced:
101
101
 
102
- | Metric | Result |
103
- | --- | ---: |
104
- | Run label | `final-pr-smoke` |
105
- | Run ID | `mt2072x6-4c0avl` |
106
- | Measured elapsed time | 31.747 seconds |
107
- | Requests completed | 1,243 |
108
- | Accepted events | 62,150 |
109
- | Accepted heartbeats | 2,486 |
110
- | Expected stored events | 59,664 |
111
- | Aggregate event delta | 59,664 |
112
- | Reconciliation | exact |
113
- | Failed requests | 0 |
114
- | Duplicate events | 0 |
115
- | Rejected events | 0 |
116
- | Accepted throughput | 1,958 events/s |
117
- | Stored throughput | 1,879 events/s |
102
+ | Metric | Result |
103
+ | ---------------------------- | ---------------------: |
104
+ | Run label | `final-pr-smoke` |
105
+ | Run ID | `mt2072x6-4c0avl` |
106
+ | Measured elapsed time | 31.747 seconds |
107
+ | Requests completed | 1,243 |
108
+ | Accepted events | 62,150 |
109
+ | Accepted heartbeats | 2,486 |
110
+ | Expected stored events | 59,664 |
111
+ | Aggregate event delta | 59,664 |
112
+ | Reconciliation | exact |
113
+ | Failed requests | 0 |
114
+ | Duplicate events | 0 |
115
+ | Rejected events | 0 |
116
+ | Accepted throughput | 1,958 events/s |
117
+ | Stored throughput | 1,879 events/s |
118
118
  | Accepted daily-rate estimate | 169,139,895 events/day |
119
- | Stored daily-rate estimate | 162,374,299 events/day |
120
- | Request latency p50 | 1,882 ms |
121
- | Request latency p95 | 2,430 ms |
122
- | Request latency p99 | 2,733 ms |
123
- | Request latency max | 2,827 ms |
119
+ | Stored daily-rate estimate | 162,374,299 events/day |
120
+ | Request latency p50 | 1,882 ms |
121
+ | Request latency p95 | 2,430 ms |
122
+ | Request latency p99 | 2,733 ms |
123
+ | Request latency max | 2,827 ms |
124
124
 
125
125
  The harness correctly marked this result as an estimate, not a certification,
126
126
  because it was shorter than 600 seconds.
@@ -6,16 +6,16 @@ and deployment, not a production SLA.
6
6
 
7
7
  ## Environment
8
8
 
9
- | Item | Value |
10
- | --- | --- |
11
- | Git commit | `e6ae92658c86c1931e697fa2a38c80fd902f7afa` |
12
- | Package version | `0.1.0-alpha.3` |
13
- | Deployment | DEV `fast-finch-7454` |
14
- | Benchmark site | `kx75pnr9wap522n0zmh4nnb4jh8cv110` |
15
- | Driver | One local Node.js process |
16
- | Reconciliation | Exact run-scoped session IDs, indexed pages of 100 |
17
- | Health before/after | `ok: true` |
18
- | GeoIP provider | Disabled |
9
+ | Item | Value |
10
+ | ------------------- | -------------------------------------------------- |
11
+ | Git commit | `e6ae92658c86c1931e697fa2a38c80fd902f7afa` |
12
+ | Package version | `0.1.0-alpha.3` |
13
+ | Deployment | DEV `fast-finch-7454` |
14
+ | Benchmark site | `kx75pnr9wap522n0zmh4nnb4jh8cv110` |
15
+ | Driver | One local Node.js process |
16
+ | Reconciliation | Exact run-scoped session IDs, indexed pages of 100 |
17
+ | Health before/after | `ok: true` |
18
+ | GeoIP provider | Disabled |
19
19
 
20
20
  All runs used `--duration 600 --certify`. Runs were sequential so the profiles
21
21
  did not compete with each other. A harness certification means the run lasted
@@ -25,12 +25,12 @@ requires at least 1,157.41 stored events/second.
25
25
 
26
26
  ## Results
27
27
 
28
- | Profile | Concurrency | Batch | Accepted | Stored | Stored/s | Certified stored/day | p95 | Exact reconciliation | 100M/day target |
29
- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- |
30
- | Lean | 100 | 50 | 952,600 | 952,600 | 1,582 | 136,654,617 | 4,543 ms | 952,600 / 952,600 | Pass |
31
- | Realistic | 75 | 50 | 885,700 | 842,080 | 1,398 | 120,808,947 | 4,247 ms | 842,080 / 842,080 | Pass |
32
- | Heavy | 100 | 20 | 353,980 | 353,980 | 588 | 50,769,480 | 5,787 ms | 353,980 / 353,980 | **Below target** |
33
- | Feature | 100 | 15 | 232,170 | 232,170 | 384 | 33,218,356 | 6,226 ms | 232,170 / 232,170 | **Below target** |
28
+ | Profile | Concurrency | Batch | Accepted | Stored | Stored/s | Certified stored/day | p95 | Exact reconciliation | 100M/day target |
29
+ | --------- | ----------: | ----: | -------: | ------: | -------: | -------------------: | -------: | -------------------- | ---------------- |
30
+ | Lean | 100 | 50 | 952,600 | 952,600 | 1,582 | 136,654,617 | 4,543 ms | 952,600 / 952,600 | Pass |
31
+ | Realistic | 75 | 50 | 885,700 | 842,080 | 1,398 | 120,808,947 | 4,247 ms | 842,080 / 842,080 | Pass |
32
+ | Heavy | 100 | 20 | 353,980 | 353,980 | 588 | 50,769,480 | 5,787 ms | 353,980 / 353,980 | **Below target** |
33
+ | Feature | 100 | 15 | 232,170 | 232,170 | 384 | 33,218,356 | 6,226 ms | 232,170 / 232,170 | **Below target** |
34
34
 
35
35
  The realistic run accepted 43,620 heartbeats, which intentionally update live
36
36
  presence without increasing persistent event counts. Its accepted-versus-stored
@@ -39,18 +39,18 @@ sustained 1,828,443 request bytes/second below the 64 KiB HTTP body limit.
39
39
 
40
40
  ## Heavy recertification after byte-accounting optimization
41
41
 
42
- Commit `4460ad34314a6e15c24a0c78aab6e6ce85db5c60` reused each event's
43
- exact UTF-8 byte measurement for both batch validation and rate limits, removing
44
- one full-payload serialization from the ingestion transaction. A new isolated
42
+ Commit `4460ad34314a6e15c24a0c78aab6e6ce85db5c60` reused each event's exact
43
+ UTF-8 byte measurement for both batch validation and rate limits, removing one
44
+ full-payload serialization from the ingestion transaction. A new isolated
45
45
  ten-minute run on 2026-08-22 used the same deployment, site, concurrency, batch
46
46
  size, and heavy payload.
47
47
 
48
- | Result | Original | Recertified | Change |
49
- | --- | ---: | ---: | ---: |
50
- | Stored events/s | 588 | 887 | +50.9% |
51
- | Certified stored/day | 50,769,480 | 76,623,702 | +50.9% |
52
- | Request bytes/s | 1,828,443 | 2,759,875 | +50.9% |
53
- | p95 latency | 5,787 ms | 3,670 ms | -36.6% |
48
+ | Result | Original | Recertified | Change |
49
+ | -------------------- | ---------: | ----------: | -----: |
50
+ | Stored events/s | 588 | 887 | +50.9% |
51
+ | Certified stored/day | 50,769,480 | 76,623,702 | +50.9% |
52
+ | Request bytes/s | 1,828,443 | 2,759,875 | +50.9% |
53
+ | p95 latency | 5,787 ms | 3,670 ms | -36.6% |
54
54
 
55
55
  The recertification remained below the separate 100 million events/day target,
56
56
  but raised the measured heavy-payload ceiling without changing payload shape or
@@ -65,7 +65,11 @@ weakening validation, rate limits, or reconciliation.
65
65
  "elapsedMs": 601726,
66
66
  "terminationReason": "duration",
67
67
  "durationCompleted": true,
68
- "certification": { "requested": true, "passed": true, "minimumDurationSeconds": 600 },
68
+ "certification": {
69
+ "requested": true,
70
+ "passed": true,
71
+ "minimumDurationSeconds": 600
72
+ },
69
73
  "concurrency": 100,
70
74
  "batchSize": 20,
71
75
  "sessionPool": 2048,
@@ -119,7 +123,11 @@ weakening validation, rate limits, or reconciliation.
119
123
  "elapsedMs": 602282,
120
124
  "terminationReason": "duration",
121
125
  "durationCompleted": true,
122
- "certification": { "requested": true, "passed": true, "minimumDurationSeconds": 600 },
126
+ "certification": {
127
+ "requested": true,
128
+ "passed": true,
129
+ "minimumDurationSeconds": 600
130
+ },
123
131
  "concurrency": 100,
124
132
  "batchSize": 50,
125
133
  "sessionPool": "unique-per-request",
@@ -171,7 +179,11 @@ weakening validation, rate limits, or reconciliation.
171
179
  "elapsedMs": 602238,
172
180
  "terminationReason": "duration",
173
181
  "durationCompleted": true,
174
- "certification": { "requested": true, "passed": true, "minimumDurationSeconds": 600 },
182
+ "certification": {
183
+ "requested": true,
184
+ "passed": true,
185
+ "minimumDurationSeconds": 600
186
+ },
175
187
  "concurrency": 75,
176
188
  "batchSize": 50,
177
189
  "sessionPool": 2048,
@@ -223,7 +235,11 @@ weakening validation, rate limits, or reconciliation.
223
235
  "elapsedMs": 602407,
224
236
  "terminationReason": "duration",
225
237
  "durationCompleted": true,
226
- "certification": { "requested": true, "passed": true, "minimumDurationSeconds": 600 },
238
+ "certification": {
239
+ "requested": true,
240
+ "passed": true,
241
+ "minimumDurationSeconds": 600
242
+ },
227
243
  "concurrency": 100,
228
244
  "batchSize": 20,
229
245
  "sessionPool": 2048,
@@ -275,7 +291,11 @@ weakening validation, rate limits, or reconciliation.
275
291
  "elapsedMs": 603868,
276
292
  "terminationReason": "duration",
277
293
  "durationCompleted": true,
278
- "certification": { "requested": true, "passed": true, "minimumDurationSeconds": 600 },
294
+ "certification": {
295
+ "requested": true,
296
+ "passed": true,
297
+ "minimumDurationSeconds": 600
298
+ },
279
299
  "concurrency": 100,
280
300
  "batchSize": 15,
281
301
  "sessionPool": 2048,