PageSourceSearch

https://app.meticulous.ai/_next/static/chunks/1833yvhf51o-u.js

js meticulous.ai collected 2026-10-03 19:04:01 UTC 768,578 bytes, 16,947 lines download raw bytes

1;!function(){try { var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof global?global:"undefined"!=typeof window?window:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&((e._debugIds|| (e._debugIds={}))[n]="ca7a8cdf-fb23-afd1-3be2-210be472a154")}catch(e){}}();
2(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,719262,e=>{"use strict";var t=e.i(318008),s=e.i(35203);e.s(["Anchor",0,e=>{let o,n=(0,s.c)(2),{id:i}=e;return n[0]!==i?(o=(0,t.jsx)("span",{id:i}),n[0]=i,n[1]=o):o=n[1],o}])},217263,e=>{e.v({"callout-card":"callout-card-module__W7FBdq__callout-card",dark:"callout-card-module__W7FBdq__dark"})},615378,127531,104302,395251,126680,e=>{"use strict";var t=e.i(318008),s=e.i(35203),o=e.i(159428),n=e.i(687652),i=e.i(293737);let a=()=>{let e=(0,n.useContext)(i.DocPageProjectContext);if(!e)throw Error("useDocPageProjectContext() must be used within a <DocPage> component");return e};e.s(["useDocPageProjectContext",0,a],127531),e.s(["ApiToken",0,()=>{let e,n,i=(0,s.c)(5),{project:r}=a(),l=null==r||(0,o.hasReadableApiToken)(r.apiToken)?void 0:o.API_TOKEN_OWNER_ONLY_MESSAGE,c=r?.apiToken;return i[0]!==c?(e=(0,o.apiTokenOrPlaceholder)(c),i[0]=c,i[1]=e):e=i[1],i[2]!==l||i[3]!==e?(n=(0,t.jsx)("code",{title:l,children:e}),i[2]=l,i[3]=e,i[4]=n):n=i[4],n}],615378);var r=e.i(944967);e.s(["Article",0,e=>{let o,n,i=(0,s.c)(3),{children:a}=e;return i[0]===Symbol.for("react.memo_cache_sentinel")?(o=(0,r.default)("py-2","w-full","min-w-0","max-w-full"),i[0]=o):o=i[0],i[1]!==a?(n=(0,t.jsx)("article",{className:o,children:a}),i[1]=a,i[2]=n):n=i[2],n}],104302),e.s(["Blockquote",0,e=>{let o,n,i=(0,s.c)(3),{children:a}=e;return i[0]===Symbol.for("react.memo_cache_sentinel")?(o=(0,r.default)("my-6","border-l-2","border-indigo-400","pl-4","text-zinc-600","dark:text-zinc-300","[&>p]:my-0","[&>p+p]:mt-3"),i[0]=o):o=i[0],i[1]!==a?(n=(0,t.jsx)("blockquote",{className:o,children:a}),i[1]=a,i[2]=n):n=i[2],n}],395251);var l=e.i(296961),c=e.i(270289),u=e.i(609828),d=e.i(932986),h=e.i(217263);let p={info:{bg:"bg-indigo-50/50 dark:bg-indigo-500/10",border:"border-indigo-200 dark:border-indigo-500/25",icon:"text-indigo-500 dark:text-indigo-400"},warning:{bg:"bg-amber-50/50 dark:bg-amber-500/10",border:"border-amber-200 dark:border-amber-500/25",icon:"text-amber-600 dark:text-amber-500"},success:{bg:"bg-green-50/50 dark:bg-green-500/10",border:"border-green-200 dark:border-green-500/25",icon:"text-green-600 dark:text-green-500"},error:{bg:"bg-red-50/50 dark:bg-red-500/10",border:"border-red-200 dark:border-red-500/25",icon:"text-red-600 dark:text-red-500"}},m={info:d.InformationCircleIcon,warning:u.ExclamationTriangleIcon,success:l.CheckCircleIcon,error:c.ExclamationCircleIcon};e.s(["CalloutCard",0,e=>{let o,n,i,a,l,c,u,d,f,g,y=(0,s.c)(23),{title:w,children:b,showIcon:v,variant:k}=e,S=void 0===v||v,T=void 0===k?"info":k,_=m[T],I=p[T];return y[0]!==I.bg||y[1]!==I.border?(o=(0,r.default)("my-6","rounded-lg","border","px-4","py-4",I.bg,I.border,h.default["callout-card"]),y[0]=I.bg,y[1]=I.border,y[2]=o):o=y[2],y[3]===Symbol.for("react.memo_cache_sentinel")?(n=(0,r.default)("flex","gap-3"),y[3]=n):n=y[3],y[4]!==_||y[5]!==S||y[6]!==I.icon?(i=S&&(0,t.jsx)("div",{className:(0,r.default)("shrink-0"),children:(0,t.jsx)(_,{className:(0,r.default)("h-6","w-6",I.icon),"aria-hidden":"true"})}),y[4]=_,y[5]=S,y[6]=I.icon,y[7]=i):i=y[7],y[8]===Symbol.for("react.memo_cache_sentinel")?(a=(0,r.default)("flex-1","min-w-0"),y[8]=a):a=y[8],y[9]!==w?(l=w&&(0,t.jsx)("h3",{className:(0,r.default)("font-semibold","text-zinc-900","dark:text-zinc-100","mb-2"),children:w}),y[9]=w,y[10]=l):l=y[10],y[11]===Symbol.for("react.memo_cache_sentinel")?(c=(0,r.default)("text-zinc-800","dark:text-zinc-200"),y[11]=c):c=y[11],y[12]!==b?(u=(0,t.jsx)("div",{className:c,children:b}),y[12]=b,y[13]=u):u=y[13],y[14]!==l||y[15]!==u?(d=(0,t.jsxs)("div",{className:a,children:[l,u]}),y[14]=l,y[15]=u,y[16]=d):d=y[16],y[17]!==d||y[18]!==i?(f=(0,t.jsxs)("div",{className:n,children:[i,d]}),y[17]=d,y[18]=i,y[19]=f):f=y[19],y[20]!==f||y[21]!==o?(g=(0,t.jsx)("div",{className:o,children:f}),y[20]=f,y[21]=o,y[22]=g):g=y[22],g}],126680)},96827,35692,810311,677136,e=>{"use strict";var t=e.i(719262),s=e.i(615378),o=e.i(104302),n=e.i(395251),i=e.i(126680),a=e.i(655176),r=e.i(318008),l=e.i(35203),c=e.i(458636),u=e.i(830616);e.i(496524);var d=e.i(412699),h=e.i(314681),p=e.i(668987),m=e.i(944967),f=e.i(727197),g=e.i(317677),y=e.i(760687),w=e.i(628144),b=e.i(147060);e.i(88865);var v=e.i(127531),k=e.i(159428);RegExp(String.raw`\{%\s*(${"code_with_project_selector|command_card_block|command_card|callout_card|callout|expand|tab|tabs|project_link|footnote|code"})\b([^%]*)%\}((?:(?!\{%)[\s\S])*?)\{%\s*/\1\s*%\}`,"g");let S=e=>"/docs"===e?"/docs.md":`${e}.md`;e.s(["docsUrlToMarkdownPath",0,S],35692);let T=e=>{let t,s,o,n,i,a=(0,l.c)(15),{idleLabel:c,activeLabel:u,isActive:d}=e;a[0]!==d?(t=(0,m.default)("col-start-1","row-start-1",{invisible:d}),a[0]=d,a[1]=t):t=a[1],a[2]!==c||a[3]!==d||a[4]!==t?(s=(0,r.jsx)("span",{className:t,"aria-hidden":d,children:c}),a[2]=c,a[3]=d,a[4]=t,a[5]=s):s=a[5];let h=!d;a[6]!==h?(o=(0,m.default)("col-start-1","row-start-1",{invisible:h}),a[6]=h,a[7]=o):o=a[7];let p=!d;return a[8]!==u||a[9]!==o||a[10]!==p?(n=(0,r.jsx)("span",{className:o,"aria-hidden":p,children:u}),a[8]=u,a[9]=o,a[10]=p,a[11]=n):n=a[11],a[12]!==s||a[13]!==n?(i=(0,r.jsxs)("span",{className:"grid",children:[s,n]}),a[12]=s,a[13]=n,a[14]=i):i=a[14],i},_=e=>{let t,s,o,n,i,a,c=(0,l.c)(12);c[0]!==e?({children:t,className:s,type:n,...o}=e,c[0]=e,c[1]=t,c[2]=s,c[3]=o,c[4]=n):(t=c[1],s=c[2],o=c[3],n=c[4]);let u=void 0===n?"button":n;return c[5]!==s?(i=(0,m.default)("inline-flex","h-7","max-w-full","shrink-0","items-center","gap-1.5","whitespace-nowrap","rounded-lg","border","border-zinc-200","bg-white","px-2.5","text-[13px]","font-medium","text-zinc-700","transition","hover:border-zinc-300","hover:bg-zinc-50","hover:text-zinc-900","dark:border-zinc-700","dark:bg-zinc-900","dark:text-zinc-300","dark:hover:border-zinc-600","dark:hover:bg-zinc-800","dark:hover:text-zinc-100",s),c[5]=s,c[6]=i):i=c[6],c[7]!==t||c[8]!==o||c[9]!==i||c[10]!==u?(a=(0,r.jsx)("button",{type:u,className:i,...o,children:t}),c[7]=t,c[8]=o,c[9]=i,c[10]=u,c[11]=a):a=c[11],a},I=()=>{let e,t,s,o,n,i=(0,l.c)(11),a=(0,w.useRouter)(),{copied:c,copyToClipboard:u}=(0,b.useCopyToClipboard)();i[0]!==a.pathname?(e=S(a.pathname),i[0]=a.pathname,i[1]=e):e=i[1];let d=e;return i[2]!==u||i[3]!==d?(t=()=>u(`${window.location.origin}${d}`),i[2]=u,i[3]=d,i[4]=t):t=i[4],i[5]===Symbol.for("react.memo_cache_sentinel")?(s=(0,r.jsx)(y.LinkIcon,{className:"h-3.5 w-3.5 shrink-0 text-zinc-400","aria-hidden":!0}),i[5]=s):s=i[5],i[6]!==c?(o=(0,r.jsx)(T,{idleLabel:"Copy .md URL",activeLabel:"Copied!",isActive:c}),i[6]=c,i[7]=o):o=i[7],i[8]!==t||i[9]!==o?(n=(0,r.jsxs)(_,{onClick:t,children:[s,o]}),i[8]=t,i[9]=o,i[10]=n):n=i[10],n};var R=e.i(183151),C=e.i(687652);let x=(0,C.createContext)(null),A=()=>(0,C.useContext)(x);e.s(["DocsPageMarkdownProvider",0,e=>{let t,s=(0,l.c)(3),{markdownSource:o,children:n}=e;return s[0]!==n||s[1]!==o?(t=(0,r.jsx)(x.Provider,{value:o,children:n}),s[0]=n,s[1]=o,s[2]=t):t=s[2],t},"useDocsPageMarkdownSource",0,A],810311);let M=()=>{let e,t,s,o,n=(0,l.c)(9),i=A(),{copied:a,copyToClipboard:c}=(0,b.useCopyToClipboard)();return i?(n[0]!==c||n[1]!==i?(e=()=>c(i),n[0]=c,n[1]=i,n[2]=e):e=n[2],n[3]===Symbol.for("react.memo_cache_sentinel")?(t=(0,r.jsx)(R.ClipboardDocumentIcon,{className:"h-3.5 w-3.5 shrink-0 text-zinc-400","aria-hidden":!0}),n[3]=t):t=n[3],n[4]!==a?(s=(0,r.jsx)(T,{idleLabel:"Copy page",activeLabel:"Copied!",isActive:a}),n[4]=a,n[5]=s):s=n[5],n[6]!==e||n[7]!==s?(o=(0,r.jsxs)(_,{onClick:e,children:[t,s]}),n[6]=e,n[7]=s,n[8]=o):o=n[8],o):null},E=e=>{let t,s,o,n,i,a=(0,l.c)(8),{children:c,id:u}=e;a[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("group","relative","min-w-0"),a[0]=t):t=a[0];let d=`#${u}`;return a[1]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)("absolute","-left-6","top-1/2","-translate-y-1/2","pr-2","opacity-0","group-hover:opacity-100","transition-opacity","text-zinc-400","hover:text-indigo-500","dark:hover:text-indigo-400","hidden","sm:block"),a[1]=s):s=a[1],a[2]===Symbol.for("react.memo_cache_sentinel")?(o=(0,r.jsx)(p.LinkIcon,{className:(0,m.default)("h-4","w-4")}),a[2]=o):o=a[2],a[3]!==d?(n=(0,r.jsx)("a",{href:d,className:s,"aria-label":"Link to this section",children:o}),a[3]=d,a[4]=n):n=a[4],a[5]!==c||a[6]!==n?(i=(0,r.jsxs)("div",{className:t,children:[n,c]}),a[5]=c,a[6]=n,a[7]=i):i=a[7],i},U=e=>{let t,s,o,n,i,a,c=(0,l.c)(11),{children:u,id:d}=e;return c[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("flex","flex-col-reverse","items-stretch","gap-0","py-8","first:pt-0","sm:flex-row","sm:items-start","sm:justify-between","sm:gap-4"),c[0]=t):t=c[0],c[1]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)(g.DOCS_HEADER_SCROLL_MARGIN_CLASS,"pt-4","text-2xl","font-bold","leading-tight","text-indigo-900","dark:text-zinc-100","wrap-break-word","sm:pt-0","sm:text-[2rem]","tracking-tight"),c[1]=s):s=c[1],c[2]!==u||c[3]!==d?(o=(0,r.jsx)(f.H1,{id:d,className:s,children:u}),c[2]=u,c[3]=d,c[4]=o):o=c[4],c[5]!==d||c[6]!==o?(n=(0,r.jsx)(E,{id:d,children:o}),c[5]=d,c[6]=o,c[7]=n):n=c[7],c[8]===Symbol.for("react.memo_cache_sentinel")?(i=(0,r.jsxs)("div",{className:"flex max-w-full shrink-0 flex-wrap items-center justify-start gap-2 sm:justify-end sm:pt-1.5",children:[(0,r.jsx)(M,{}),(0,r.jsx)(I,{})]}),c[8]=i):i=c[8],c[9]!==n?(a=(0,r.jsxs)("div",{className:t,children:[n,i]}),c[9]=n,c[10]=a):a=c[10],a},L=e=>{let t,s,o,n,i=(0,l.c)(8),{children:a,id:c}=e;return i[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("mt-12","mb-6","first:mt-0"),i[0]=t):t=i[0],i[1]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)(g.DOCS_HEADER_SCROLL_MARGIN_CLASS,"text-xl","leading-8","font-semibold","text-indigo-900","dark:text-zinc-100","sm:text-[1.375rem]","tracking-tight"),i[1]=s):s=i[1],i[2]!==a||i[3]!==c?(o=(0,r.jsx)(f.H2,{id:c,className:s,children:a}),i[2]=a,i[3]=c,i[4]=o):o=i[4],i[5]!==c||i[6]!==o?(n=(0,r.jsx)("div",{className:t,children:(0,r.jsx)(E,{id:c,children:o})}),i[5]=c,i[6]=o,i[7]=n):n=i[7],n},P=e=>{let t,s,o,n,i=(0,l.c)(8),{children:a,id:c}=e;return i[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("mt-8","mb-4"),i[0]=t):t=i[0],i[1]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)(g.DOCS_HEADER_SCROLL_MARGIN_CLASS,"text-lg","leading-7","font-semibold","text-indigo-900","dark:text-zinc-100","sm:text-xl"),i[1]=s):s=i[1],i[2]!==a||i[3]!==c?(o=(0,r.jsx)(f.H3,{id:c,className:s,children:a}),i[2]=a,i[3]=c,i[4]=o):o=i[4],i[5]!==c||i[6]!==o?(n=(0,r.jsx)("div",{className:t,children:(0,r.jsx)(E,{id:c,children:o})}),i[5]=c,i[6]=o,i[7]=n):n=i[7],n}
2,O=e=>{let t=(0,l.c)(13),{level:s,children:o,id:n}=e;switch(s){case 1:{let e;return t[0]!==o||t[1]!==n?(e=(0,r.jsx)(U,{id:n,children:o}),t[0]=o,t[1]=n,t[2]=e):e=t[2],e}case 2:{let e;return t[3]!==o||t[4]!==n?(e=(0,r.jsx)(L,{id:n,children:o}),t[3]=o,t[4]=n,t[5]=e):e=t[5],e}case 3:{let e;return t[6]!==o||t[7]!==n?(e=(0,r.jsx)(P,{id:n,children:o}),t[6]=o,t[7]=n,t[8]=e):e=t[8],e}default:{let e,i=4===s?"h4":5===s?"h5":"h6";return t[9]!==i||t[10]!==o||t[11]!==n?(e=(0,r.jsx)(i,{id:n,className:g.DOCS_HEADER_SCROLL_MARGIN_CLASS,children:o}),t[9]=i,t[10]=o,t[11]=n,t[12]=e):e=t[12],e}}};function N(){return(0,d.toast)({title:"Agent instructions copied"})}function D(){window.navigator.clipboard.writeText((0,c.buildAgentInstructionsBody)()).then(N).catch(h.handleError)}let j=e=>{let t,s,o=(0,l.c)(5),{children:n,className:i}=e;return o[0]!==i?(t=(0,m.default)("card","bg-black","overflow-hidden","text-white","text-base","font-normal","dark",i),o[0]=i,o[1]=t):t=o[1],o[2]!==n||o[3]!==t?(s=(0,r.jsx)("div",{className:t,children:n}),o[2]=n,o[3]=t,o[4]=s):s=o[4],s},F=e=>{let t,s=(0,l.c)(3),{children:o,action:n}=e;return s[0]!==n||s[1]!==o?(t=n?(0,r.jsxs)("div",{className:"flex items-baseline justify-between pr-4",children:[(0,r.jsx)("h3",{className:(0,m.default)("card-title","mb-0!"),children:o}),n]}):(0,r.jsx)("h3",{className:(0,m.default)("card-title"),children:o}),s[0]=n,s[1]=o,s[2]=t):t=s[2],t};var H=e.i(24777),$=e.i(244142),q=e.i(582238),B=e.i(305655),G=e.i(44167),W=e.i(664396);let z=()=>{let e,t,s,o=(0,l.c)(9),{data:n}=(0,G.useUserContext)(),i=!n?.authInfo?.isSignedIn;o[0]!==i?(e={skip:i},o[0]=i,o[1]=e):e=o[1];let{data:a}=(0,W.useProjects)(e),{project:c,setProject:u}=(0,v.useDocPageProjectContext)();o[2]!==a||o[3]!==u?(t=e=>{u(a?.find(t=>t.id===e.id)??null)},o[2]=a,o[3]=u,o[4]=t):t=o[4];let d=t;return o[5]!==d||o[6]!==a||o[7]!==c?(s=(0,r.jsx)(V,{projects:a,selectedProject:c,setProject:d}),o[5]=d,o[6]=a,o[7]=c,o[8]=s):s=o[8],s},V=e=>{let t,s,o,n=(0,l.c)(8),{projects:i,selectedProject:a,setProject:c}=e;if(!i||!a)return null;let u=`${a.organization.name} / ${a.name}`;return n[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("px-4","py-1","sm:px-6","sm:flex","sm:flex-row","sm:justify-start","sm:items-baseline"),n[0]=t):t=n[0],n[1]!==u||n[2]!==i?(s=e=>{let{open:t}=e;return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(H.Listbox.Label,{className:(0,m.default)("block","text-sm","font-medium","text-zinc-800","dark:text-zinc-100","mr-2"),children:"Project"}),(0,r.jsxs)("div",{className:(0,m.default)("mt-1","relative","sm:ml-2","sm:flex-1"),children:[(0,r.jsxs)(B.StyledListboxButton,{children:[(0,r.jsx)("span",{className:(0,m.default)("block","truncate"),children:u}
2),(0,r.jsx)("span",{className:(0,m.default)("absolute","inset-y-0","right-0","flex","items-center","pr-2","pointer-events-none"),children:(0,r.jsx)(q.ChevronUpDownIcon,{className:(0,m.default)("h-5","w-5","text-zinc-800","dark:text-zinc-100"),"aria-hidden":"true"})})]}),(0,r.jsx)($.Transition,{show:t,as:C.Fragment,leave:(0,m.default)("transition","ease-in","duration-100"),leaveFrom:(0,m.default)("opacity-100"),leaveTo:(0,m.default)("opacity-0"),children:(0,r.jsx)(B.StyledListboxOptions,{children:i.map(K)})})]})]})},n[1]=u,n[2]=i,n[3]=s):s=n[3],n[4]!==a||n[5]!==c||n[6]!==s?(o=(0,r.jsx)("div",{className:t,children:(0,r.jsx)(H.Listbox,{value:a,onChange:c,children:s})}),n[4]=a,n[5]=c,n[6]=s,n[7]=o):o=n[7],o};function K(e){return(0,r.jsx)(B.StyledListboxOption,{value:e,children:`${e.organization.name} / ${e.name}`},e.id)}var Y=e.i(551481),J=e.i(919414);e.i(395774);let X=(0,C.createContext)(null);var Q=e.i(989571),Z=e.i(444717);let ee=e=>{let t,s,o=(0,l.c)(9),{title:n,children:i,buttonClassName:a,panelClassName:c,icon:u,defaultOpen:d}=e,h=void 0!==d&&d;return o[0]!==a||o[1]!==i||o[2]!==u||o[3]!==c||o[4]!==n?(t=e=>{let{open:t}=e;return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(Q.Disclosure.Button,{className:(0,m.default)("mt-6 mb-4 flex items-center gap-1 text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100 transition-colors",a),children:[(0,r.jsx)(Z.ChevronRightIcon,{className:(0,m.default)("h-4 w-4 transition-transform duration-200",t&&"rotate-90")}),u&&(0,r.jsx)("span",{className:"h-4 w-4 mr-1 text-zinc-400 shrink-0",children:u}),(0,r.jsx)("span",{children:n})]}),(0,r.jsx)(Q.Disclosure.Panel,{className:(0,m.default)("mb-6","overflow-hidden","rounded-md","p-6",c),children:i})]})},o[0]=a,o[1]=i,o[2]=u,o[3]=c,o[4]=n,o[5]=t):t=o[5],o[6]!==h||o[7]!==t?(s=(0,r.jsx)(Q.Disclosure,{defaultOpen:h,children:t}),o[6]=h,o[7]=t,o[8]=s):s=o[8],s};var et=e.i(549877);let es=e=>{let t,s,o,n=(0,l.c)(5),{src:i,alt:a}=e;return n[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("w-full"),n[0]=t):t=n[0],n[1]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)("w-full","h-auto","object-contain","rounded-md"),n[1]=s):s=n[1],n[2]!==a||n[3]!==i?(o=(0,r.jsx)("span",{className:t,children:(0,r.jsx)("img",{src:i,alt:a,className:s})}),n[2]=a,n[3]=i,n[4]=o):o=n[4],o};e.s(["Image",0,es],677136);var eo=e.i(39786),en=e.i(372222),ei=e.i(845722);let ea={Anchor:t.Anchor,ApiToken:s.ApiToken,StandaloneApiToken:()=>{let e,t,s,o,n,i=(0,l.c)(10),{project:a}=(0,v.useDocPageProjectContext)(),c=(0,k.apiTokenOrPlaceholder)(a?.apiToken),u=(0,C.useRef)(null),d=c.trim();return i[0]!==d?(e=(0,r.jsx)("pre",{ref:u,className:"overflow-x-auto p-4 text-sm text-zinc-800 dark:text-stone-100",children:(0,r.jsx)("code",{children:d})}),i[0]=d,i[1]=e):e=i[1],i[2]===Symbol.for("react.memo_cache_sentinel")?(t=(0,r.jsx)(b.TransientCopyButton,{code:u}),i[2]=t):t=i[2],i[3]!==e?(s=(0,r.jsx)("div",{className:"card bg-black overflow-hidden text-white text-base font-normal dark my-4",children:(0,r.jsx)("div",{className:"group",children:(0,r.jsxs)("div",{className:"relative",children:[e,t]})})}),i[3]=e,i[4]=s):s=i[4],i[5]!==a?(o=null!=a&&!(0,k.hasReadableApiToken)(a.apiToken)&&(0,r.jsx)("p",{className:"text-sm text-zinc-400 -mt-2 mb-4",children:k.API_TOKEN_OWNER_ONLY_MESSAGE}),i[5]=a,i[6]=o):o=i[6],i[7]!==s||i[8]!==o?(n=(0,r.jsxs)(r.Fragment,{children:[s,o]}),i[7]=s,i[8]=o,i[9]=n):n=i[9],n},Article:o.Article,Blockquote:n.Blockquote,CalloutCard:i.CalloutCard,Code:a.Code,AgentInstructionsHeading:()=>{let e,t,s=(0,l.c)(2);return s[0]===Symbol.for("react.memo_cache_sentinel")?(e=(0,r.jsx)(O,{level:2,id:"instructions",children:"Instructions"}),s[0]=e):e=s[0],s[1]===Symbol.for("react.memo_cache_sentinel")?(t=(0,r.jsxs)("div",{className:"mt-12 mb-6 flex items-center justify-between gap-4 [&>div]:my-0",children:[e,(0,r.jsxs)("button",{type:"button",onClick:D,className:"inline-flex shrink-0 items-center gap-1.5 rounded-sm border border-zinc-300 bg-white px-2.5 py-1.5 text-sm text-zinc-700 transition-colors hover:bg-zinc-50 hover:text-zinc-900 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-zinc-100",children:[(0,r.jsx)(u.ClipboardIcon,{className:"h-4 w-4","aria-hidden":"true"}),"Copy"]})]}),s[1]=t):t=s[1],t},CodeWithProjectSelectorCard:e=>{let t,s,o,n,i=(0,l.c)(5),{children:a}=e;return i[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("my-4","py-2","flex","flex-col","gap-4"),s=(0,r.jsx)(z,{}),i[0]=t,i[1]=s):(t=i[0],s=i[1]),i[2]===Symbol.for("react.memo_cache_sentinel")?(o=(0,m.default)("card-body"),i[2]=o):o=i[2],i[3]!==a?(n=(0,r.jsxs)("div",{className:t,children:[s,(0,r.jsx)(j,{children:(0,r.jsx)("div",{className:o,children:a})})]}),i[3]=a,i[4]=n):n=i[4],n},DocsTabs:e=>{let t,s,o,n,i,a,c,u,d,p,f,g=(0,l.c)(36),{labels:y,children:b,direction:v,noTabSelectedByDefault:k,defaultSelectedTabLabel:S,tabNameSpace:T}=e,_=T??"tab",[I,R]=(0,C.useState)(k?null:S??y[0]);g[0]!==I||g[1]!==y?(t=I?y.indexOf(I):-1,g[0]=I,g[1]=y,g[2]=t):t=g[2];let x=t,A=window.location.search;g[3]!==y||g[4]!==_?(s=()=>{let e=new URLSearchParams(A||"").get(_),t=y.find(t=>t===e);t&&R(t)},o=[A,y,_],g[3]=y,g[4]=_,g[5]=s,g[6]=o):(s=g[5],o=g[6]),(0,C.useEffect)(s,o);let M=(0,w.useRouter)();g[7]!==y||g[8]!==_||g[9]!==M?(n=e=>{let t=y[e];M.push({pathname:M.pathname,query:{...M.query,[_]:t}},void 0,{shallow:!0,scroll:!1}
2).catch(h.handleError)},g[7]=y,g[8]=_,g[9]=M,g[10]=n):n=g[10];let E=n;if(g[11]!==v?(i=(0,m.default)("mb-8","min-w-0","max-w-full","grid"===v?"grid grid-cols-1 gap-2 sm:grid-cols-2":(0,m.default)("flex border-b border-zinc-200 dark:border-zinc-800",(!v||"horizontal"===v)&&"flex-row overflow-x-auto scrollbar-hide gap-0","vertical"===v&&"flex-col")),g[11]=v,g[12]=i):i=g[12],g[13]!==I||g[14]!==v||g[15]!==y){let e;g[17]!==I||g[18]!==v?(e=e=>(0,r.jsx)(J.Tab,{onClick:()=>R(e),className:(0,m.default)("text-sm font-medium transition-colors duration-200 focus:outline-hidden","grid"===v?(0,m.default)("min-w-0 max-w-full py-2.5 px-3 sm:px-4 rounded-md border text-center whitespace-normal wrap-break-word",e===I?"border-indigo-500 bg-indigo-500/10 text-indigo-500 dark:text-indigo-400":"border-zinc-200 text-zinc-600 hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"):(0,m.default)("whitespace-nowrap py-2 px-4 border-b-2 -mb-px",e===I?"border-indigo-500 text-indigo-500 dark:text-indigo-400":"border-transparent text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200")),children:e},e),g[17]=I,g[18]=v,g[19]=e):e=g[19],a=y.map(e),g[13]=I,g[14]=v,g[15]=y,g[16]=a}else a=g[16];return g[20]!==i||g[21]!==a?(c=(0,r.jsx)(J.Tab.List,{className:i,children:a}),g[20]=i,g[21]=a,g[22]=c):c=g[22],g[23]!==I||g[24]!==_?(u={currentTab:I,nameSpace:_},g[23]=I,g[24]=_,g[25]=u):u=g[25],g[26]!==b?(d=(0,r.jsx)(J.Tab.Panels,{children:b}),g[26]=b,g[27]=d):d=g[27],g[28]!==u||g[29]!==d?(p=(0,r.jsx)(X.Provider,{value:u,children:d}),g[28]=u,g[29]=d,g[30]=p):p=g[30],g[31]!==E||g[32]!==x||g[33]!==p||g[34]!==c?(f=(0,r.jsxs)(J.Tab.Group,{onChange:E,defaultIndex:x,children:[c,p]}),g[31]=E,g[32]=x,g[33]=p,g[34]=c,g[35]=f):f=g[35],f},DocsTab:e=>{let t,s=(0,l.c)(3),{label:o,children:n}=e,i=(0,C.useContext)(X);if(!i||o!==i.currentTab){let e;return s[0]===Symbol.for("react.memo_cache_sentinel")?(e=(0,r.jsx)(r.Fragment,{}),s[0]=e):e=s[0],e}return s[1]!==n?(t=(0,r.jsx)(r.Fragment,{children:n}),s[1]=n,s[2]=t):t=s[2],t},DocsExpand:e=>{let t,s,o=(0,l.c)(5);return o[0]!==e.panelClassName?(t=(0,m.default)("bg-zinc-50 border border-zinc-200 dark:bg-zinc-800/50 dark:border-zinc-800",e.panelClassName),o[0]=e.panelClassName,o[1]=t):t=o[1],o[2]!==e||o[3]!==t?(s=(0,r.jsx)(ee,{...e,panelClassName:t}),o[2]=e,o[3]=t,o[4]=s):s=o[4],s},Expand:ee,CommandCard:e=>{let t,s,o,n,i,a,c,u=(0,l.c)(14),{title:d,hideProjectSelector:h,children:p}=e;return u[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("my-4","py-2","flex","flex-col","gap-2"),u[0]=t):t=u[0],u[1]!==h?(s=!h&&(0,r.jsx)(z,{}),u[1]=h,u[2]=s):s=u[2],u[3]!==d?(o=d&&(0,r.jsx)(F,{children:d}),u[3]=d,u[4]=o):o=u[4],u[5]===Symbol.for("react.memo_cache_sentinel")?(n=(0,m.default)("card-body"),u[5]=n):n=u[5],u[6]!==p?(i=(0,r.jsx)("div",{className:n,children:p}),u[6]=p,u[7]=i):i=u[7],u[8]!==o||u[9]!==i?(a=(0,r.jsxs)(j,{children:[o,i]}),u[8]=o,u[9]=i,u[10]=a):a=u[10],u[11]!==s||u[12]!==a?(c=(0,r.jsxs)("div",{className:t,children:[s,a]}),u[11]=s,u[12]=a,u[13]=c):c=u[13],c},CommandCardBlock:e=>{let t,s,o=(0,l.c)(5),{title:n,children:i}=e;return o[0]!==n?(t=n&&(0,r.jsx)("h4",{className:(0,m.default)("py-2","font-code","font-medium"),children:n}),o[0]=n,o[1]=t):t=o[1],o[2]!==i||o[3]!==t?(s=(0,r.jsxs)("div",{className:Y.REDACTION_CLASSES,children:[t,i]}),o[2]=i,o[3]=t,o[4]=s):s=o[4],s},Fence:e=>{let t,s=(0,l.c)(4),{language:o,label:n,children:i}=e;return s[0]!==i||s[1]!==n||s[2]!==o?(t=(0,r.jsx)(j,{className:"my-6",children:(0,r.jsx)(et.CodeBlock,{language:o,label:n,children:i})}),s[0]=i,s[1]=n,s[2]=o,s[3]=t):t=s[3],t},Footnote:e=>{let t,s,o=(0,l.c)(3),{children:n}=e;return o[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("[&_p]:text-sm","[&_p]:leading-6","[&_p]:text-zinc-500","dark:[&_p]:text-zinc-400"),o[0]=t):t=o[0],o[1]!==n?(s=(0,r.jsx)("div",{className:t,children:n}),o[1]=n,o[2]=s):s=o[2],s},Heading:O,Image:es,Link:e=>{let t,s,o,n=(0,l.c)(7),{href:i,children:a}=e;n[0]!==i?(t=(0,eo.isInternalDocLink)(i),n[0]=i,n[1]=t):t=n[1];let c=t;n[2]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)("link","dark:text-indigo-400","break-all","sm:break-normal"),n[2]=s):s=n[2];let u=!c;return n[3]!==a||n[4]!==i||n[5]!==u?(o=(0,r.jsx)(en.Link,{className:s,href:i,external:u,children:a}),n[3]=a,n[4]=i,n[5]=u,n[6]=o):o=n[6],o},List:e=>{let t,s,o=(0,l.c)(6),{ordered:n,children:i}=e,a=n?"ol":"ul",c=n?"list-decimal":"list-disc";return o[0]!==c?(t=(0,m.default)(c,"list-outside","ml-8","my-4","space-y-2","text-zinc-700","dark:text-zinc-300","leading-6","[&_ul]:mt-2","[&_ul]:mb-0","[&_ol]:mt-2","[&_ol]:mb-0"),o[0]=c,o[1]=t):t=o[1],o[2]!==a||o[3]!==i||o[4]!==t?(s=(0,r.jsx)(a,{className:t,children:i}),o[2]=a,o[3]=i,o[4]=t,o[5]=s):s=o[5],s},Paragraph:e=>{let t,s,o=(0,l.c)(3),{children:n}=e;return o[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("my-4","leading-7","text-zinc-600","dark:text-zinc-300","wrap-break-word","wrap-anywhere"),o[0]=t):t=o[0],o[1]!==n?(s=(0,r.jsx)("p",{className:t,children:n}),o[1]=n,o[2]=s):s=o[2],s},ProjectLink:e=>{let t,s,o,n=(0,l.c)(10),{children:i}=e,{project:a}=(0,v.useDocPageProjectContext)();if(!a){let e,t;return n[0]===Symbol.for("react.memo_cache_sentinel")?(e=(0,m.default)("link"),n[0]=e):e=n[0],n[1]!==i?(t=(0,r.jsx)(en.Link,{className:e,href:"/",external:!0,children:i}),n[1]=i,n[2]=t):t=n[2],t}n[3]!==a.name||n[4]!==a.organization.name?(t=(0,ei.getProjectUrl)({organizationName:a.organization.name,projectName:a.name}),n[3]=a.name,n[4]=a.organization.name,n[5]=t):t=n[5];let c=t;return n[6]===Symbol.for("react.memo_cache_sentinel")?(s=(0,m.default)("link"),n[6]=s):s=n[6],n[7]!==i||n[8]!==c?(o=(0,r.jsx)(en.Link,{className:s,href:c,external:!0,children:i}),n[7]=i,n[8]=c,n[9]=o):o=n[9],o},ProjectName:()=>{let e,t=(0,l.c)(2),{project:s}=(0,v.useDocPageProjectContext)(),o=s?.name||"<your-project-name>";return t[0]!==o?(e=(0,r.jsx)("code",{children:o}),t[0]=o,t[1]=e):e=t[1],e},ProjectRecordingToken:()=>{let e,t=(0,l.c)(2),{project:s}=(0,v.useDocPageProjectContext)(),o=s?.recordingToken||"<RECORDING_TOKEN>";return t[0]!==o?(e=(0,r.jsx)("code",{children:o}),t[0]=o,t[1]=e):e=t[1],e},ProjectSlug:()=>{let e,t=(0,l.c)(2),{project:s}=(0,v.useDocPageProjectContext)(),o=s?`${s.organization.name}/${s.name}`:"<ORGANIZATION>/<PROJECT>";return t[0]!==o?(e=(0,r.jsx)(r.Fragment,{children:o}),t[0]=o,t[1]=e):e=t[1],e},Table:e=>{let t,s,o,n=(0,l.c)(4),{children:i}=e;return n[0]===Symbol.for("react.memo_cache_sentinel")?(t=(0,m.default)("my-6","w-full","overflow-x-auto","rounded-lg","border","border-zinc-200","dark:border-zinc-800"),s=(0,m.default)("w-full","border-collapse","text-left","text-sm","text-zinc-700","dark:text-zinc-300","[&_th]:border-b [&_th]:border-r [&_th]:border-zinc-200 [&_th]:bg-zinc-50 [&_th]:px-3.5 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold [&_th]:text-zinc-900 [&_th]:align-bottom [&_th:last-child]:border-r-0","dark:[&_th]:border-zinc-800 dark:[&_th]:bg-zinc-800/60 dark:[&_th]:text-zinc-100","[&_td]:border-b [&_td]:border-r [&_td]:border-zinc-200 [&_td]:px-3.5 [&_td]:py-2 [&_td]:align-top [&_td:last-child]:border-r-0","dark:[&_td]:border-zinc-800","[&_tbody_tr:nth-child(even)]:bg-zinc-50/60","dark:[&_tbody_tr:nth-child(even)]:bg-zinc-800/30","[&_tbody_tr:last-child_td]:border-b-0","[&_td_code]:whitespace-nowrap [&_th_code]:whitespace-nowrap"),n[0]=t,n[1]=s):(t=n[0],s=n[1]),n[2]!==i?(o=(0,r.jsx)("div",{className:t,children:(0,r.jsx)("table",{className:s,children:i})}),n[2]=i,n[3]=o):o=n[3],o}};e.s(["components",0,ea],96827)},293737,e=>{"use strict";var t=e.i(318008),s=e.i(35203),o=e.i(687652),n=e.i(44167),i=e.i(664396);let a=(0,o.createContext)(null);e.s(["DocPageProjectContext",0,a,"DocPageProjectContextProvider",0,e=>{let r,l,c,u,d,h=(0,s.c)(11),{children:p}=e,{data:m}=(0,n.useUserContext)(),f=!m?.authInfo?.isSignedIn;h[0]!==f?(r={skip:f},h[0]=f,h[1]=r):r=h[1];let{data:g}=(0,i.useProjects)(r),[y,w]=(0,o.useState)(null);h[2]!==y?(l={project:y,setProject:w},h[2]=y,h[3]=l):l=h[3];let b=l;return h[4]!==y||h[5]!==g?(c=()=>{!y&&g?.length&&w(g[0])},u=[y,g],h[4]=y,h[5]=g,h[6]=c,h[7]=u):(c=h[6],u=h[7]),(0,o.useEffect)(c,u),h[8]!==p||h[9]!==b?(d=(0,t.jsx)(a.Provider,{value:b,children:p}),h[8]=p,h[9]=b,h[10]=d):d=h[10],d}])},317677,e=>{"use strict";e.s(["DOCS_CHROME_CLASS",0,"docs-chrome font-sans text-[15px] antialiased text-zinc-700 bg-white dark:text-zinc-300 dark:bg-zinc-900","DOCS_HEADER_MAIN_MIN_HEIGHT_CLASS",0,"min-h-[calc(100vh-5.25rem)]","DOCS_HEADER_SCROLL_MARGIN_CLASS",0,"scroll-mt-12 lg:scroll-mt-21","DOCS_HEADER_SIDEBAR_HEIGHT_CLASS",0,"lg:h-[c
2alc(100vh-5.25rem)]","DOCS_HEADER_TOP_OFFSET_CLASS",0,"lg:top-21","DOCS_TOP_BAR_HEIGHT_CLASS",0,"h-12","DOCS_TOP_BAR_HEIGHT_PX",0,48,"DOCS_TOP_BAR_ONLY_MAIN_MIN_HEIGHT_CLASS",0,"min-h-[calc(100vh-3rem)]","DOCS_TOP_BAR_ONLY_SIDEBAR_HEIGHT_CLASS",0,"lg:h-[calc(100vh-3rem)]","DOCS_TOP_BAR_ONLY_TOP_OFFSET_CLASS",0,"lg:top-12"])},715524,326659,368054,e=>{"use strict";var t=e.i(318008),s=e.i(35203),o=e.i(13158),n=e.i(627194),i=e.i(707740),a=e.i(944967),r=e.i(398145),l=e.i(628144),c=e.i(122047),u=e.i(724677),d=e.i(552099),h=e.i(828220),p=e.i(452290),m=e.i(727685),f=e.i(317677),g=e.i(39786);let y=(e,t)=>e===t||e.startsWith(`${t}/`),w=[{name:"Start building",isSectionLabel:!0,childItems:[{name:"Introduction",href:g.DOCS_URL},{name:"Connect & set up",href:g.ONBOARDING_GUIDE_URL},{name:"Install the Recorder",defaultExpanded:!1,childItems:[{name:"Overview",href:g.INSTALL_RECORDER_URL},{name:"Via a Script Tag",href:g.INSTALL_RECORDER_AS_SCRIPT_TAG_URL},{name:"Via an NPM Package",href:g.INSTALL_RECORDER_AS_NPM_DEPENDENCY_URL}]},{name:"Setup Tests to Run In CI",defaultExpanded:!1,childItems:[{name:"Overview",href:g.CI_SETUP_URL},{name:"Via your CI Pipeline",href:g.GITHUB_ACTIONS_SETUP_URL},{name:"Via Vercel, Netlify or Other Preview URLs",href:g.CLOUD_REPLAY_URL}]}]},{name:"Overview",isSectionLabel:!0,childItems:[{name:"Architecture Overview",href:g.ARCHITECTURE_OVERVIEW_URL},{name:"Network Recording & Patching",href:g.NETWORK_RECORDING_AND_PATCHING_URL},{name:"Glossary",href:g.GLOSSARY_URL},{name:"Recording",defaultExpanded:!1,childItems:[{name:"Manually Record a Test",href:g.RECORD_A_TEST_MANUALLY_URL},{name:"Recorder Developer Tools",href:g.RECORDER_DEVELOPER_TOOLS_URL},{name:"Ingest Existing Tests",href:g.INGEST_EXISTING_TESTS_URL},{name:"Control What Data is Recorded",href:g.CONTROLLING_DATA_RECORDED_URL},{name:"Control When Recording Starts and Stops",href:g.CONTROLLING_WHEN_RECORDING_STARTS_AND_STOPS_URL},{name:"Redaction",href:g.REDACTION_URL},{name:"Record Custom Values",href:g.RECORD_CUSTOM_VALUES_URL},{name:"Record the Context of a User Session",href:g.RECORD_SESSION_CONTEXT_URL},{name:"Using the Custom Event API",href:g.USE_CUSTOM_EVENT_API_URL},{name:"Handle File Uploads",href:g.HANDLE_FILE_UPLOADS_URL}]},{name:"Test Execution & CI",defaultExpanded:!1,childItems:[{name:"Select Which Sessions to Run",href:g.TESTING_POOL_URL},{name:"Manually Creating Deployments on GitHub",href:g.CREATE_DEPLOYMENTS_ON_GITHUB_URL},{name:"Ensure Base Test Runs are Available",href:g.PREPARE_FOR_TESTS_URL},{name:"Block Merging PRs with Unacknowledged Diffs",href:g.MAKE_CHECK_BLOCKING_URL},{name:"Retry a Test Run",href:g.RETRY_TEST_RUN_URL},{name:"Detect Diffs Locally",href:g.DETECT_DIFFS_LOCALLY_URL},{name:"Enable Source Coverage",href:g.ENABLE_SOURCE_COVERAGE_URL},{name:"Configure Ignore Patterns",href:g.CONFIGURE_IGNORE_PATTERNS_URL},{name:"Blocking Requests During Replay",href:g.BLOCKED_REQUESTS_URL},{name:"Advanced",defaultExpanded:!1,childItems:[{name:"Incremental Asset Upload",href:g.INCREMENTAL_ASSET_UPLOAD_URL},{name:"Filter Sessions by Start URL",href:g.FILTER_SESSIONS_BY_START_URL_URL},{name:"Companion Assets",href:g.COMPANION_ASSETS_ADVANCED_URL}]}]},{name:"Framework & App Configuration",defaultExpanded:!1,childItems:[{name:"Next.js App Router",href:g.NEXTJS_APP_ROUTER_ADDITIONAL_SETUP_URL},{name:"Next.js Pages Router",href:g.NEXTJS_PAGES_ROUTER_URL},{name:"React with Vite",href:g.REACT_VITE_URL},{name:"React with Create React App",href:g.REACT_CRA_URL},{name:"Vue with Vite",href:g.VUE_VITE_URL},{name:"Angular CLI",href:g.ANGULAR_CLI_URL},{name:"Test Multiple Apps or App Variants",href:g.TESTING_MULTIPLE_APPS_URL},{name:"Testing Feature Flags",href:g.TESTING_FEATURE_FLAGS},{name:"Detect If Running as a Meticulous Test",href:g.METICULOUS_WINDOW_OBJECT_URL},{name:"Record and Simulate on Different Environments",href:g.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}]}]},{name:"Troubleshooting",isSectionLabel:!0,childItems:[{name:"Recorder",defaultExpanded:!1,childItems:[{name:"Troubleshoot Recorder",href:g.TROUBLESHOOT_RECORDER_URL},{name:"Required CSP Exceptions for Recorder",href:g.RECORDER_CSP_EXCEPTIONS_URL},{name:"Ensure Recorder Captures All Requests",href:g.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL}]},{name:"Simulations & Diffs",defaultExpanded:!1,childItems:[{name:"Troubleshoot Failed Simulations",href:g.TROUBLESHOOTING_FAILED_SIMULATIONS_URL},{name:"Fix False Positive Diffs, or Ignore Elements",href:g.FIX_FALSE_POSITIVES_URL},{name:"Inaccurate Simulation Warnings",href:g.TROUBLESHOOT_REPLAY_ACCURACY_URL}]},{name:"Authentication",defaultExpanded:!1,childItems:[{name:"Troubleshoot Authentication and Authorisation",href:g.TROUBLESHOOT_AUTH_URL},{name:"Simulating Auth",href:g.AUTH_ENABLING_FULL_AUTH_URL},{name:"Bypassing Auth",href:g.AUTH_BYPASSING_AUTH_URL}]},{name:"Warnings in Meticulous UI",defaultExpanded:!1,childItems:[{name:"Inaccurate Simulation Warnings",href:g.TROUBLESHOOT_REPLAY_ACCURACY_URL}]}]},{name:"Reference",isSectionLabel:!0,childItems:[{name:"CLI Commands",href:g.CLI_COMMANDS_URL},{name:"Environment Variables",href:g.ENVIRONMENT_VARIABLES_URL},{name:"Performance API",href:g.PERFORMANCE_API_URL}]}],b=[{name:"What's new",href:g.AGENTS_WHATS_NEW_URL},{name:"Setup",href:g.AGENTS_SETUP_URL},{name:"CLI Commands",href:g.AGENTS_CLI_COMMANDS_URL},{name:"MCP Server",href:g.AGENTS_MCP_SERVER_URL},{name:"Skills & Use cases",href:g.AGENTS_SKILLS_URL},{name:"Agent swarm (Beta)",href:g.AGENT_SWARM_DOCS_URL}],v=[{name:"Built-in checks",isSectionLabel:!0,childItems:[{name:"Overview",href:g.BUILT_IN_CHECKS_URL}
2,{name:"Accessibility",href:g.BUILT_IN_CHECKS_ACCESSIBILITY_URL},{name:"Network requests",href:g.BUILT_IN_CHECKS_NETWORK_REQUESTS_URL},{name:"React component renders",href:g.BUILT_IN_CHECKS_REACT_COMPONENT_RENDERS_URL}]},{name:"Custom checks",isSectionLabel:!0,childItems:[{name:"Overview",href:g.CUSTOM_CHECKS_URL},{name:"Writing a custom check",href:g.CUSTOM_CHECKS_WRITING_A_CUSTOM_CHECK_URL},{name:"Recording custom snapshots",href:g.CUSTOM_CHECKS_RECORDING_CUSTOM_DATA_URL},{name:"Best practices",href:g.CUSTOM_CHECKS_BEST_PRACTICES_URL},{name:"Built-in snapshot types",href:g.CUSTOM_CHECKS_BUILT_IN_SNAPSHOT_TYPES_URL}]}],k=[{id:"get-started",name:"Get started",href:g.DOCS_URL,matches:e=>e===g.DOCS_URL||!(!y(e,"/docs")||y(e,"/docs/agents")||y(e,"/docs/custom-checks")||y(e,"/docs/built-in-checks")||y(e,g.FAQ_AND_TROUBLESHOOTING_URL)),sidebarItems:w},{id:"ai-agents",name:"AI agents",href:g.AGENTS_SETUP_URL,matches:e=>y(e,"/docs/agents"),sidebarItems:b},{id:"non-visual-checks",name:"Non-visual checks",href:g.BUILT_IN_CHECKS_URL,matches:e=>y(e,g.BUILT_IN_CHECKS_URL)||y(e,g.CUSTOM_CHECKS_URL),sidebarItems:v},{id:"faq",name:"FAQ",href:g.FAQ_AND_TROUBLESHOOTING_URL,matches:e=>y(e,g.FAQ_AND_TROUBLESHOOTING_URL),sidebarItems:[{name:"FAQ",href:g.FAQ_AND_TROUBLESHOOTING_URL}]}],S=[{id:"docs",name:"Docs",href:g.DOCS_URL},{id:"changelog",name:"Changelog",href:g.CHANGELOG_URL}],T=e=>y(e,g.CHANGELOG_URL),_=e=>k.find(t=>t.matches(e))??k[0];e.s(["DOCS_NAV_SECTIONS",0,k,"DOCS_PRODUCT_AREAS",0,S,"getActiveDocsSection",0,_,"isDocsChangelogPath",0,T],326659);let I=()=>{let e,n,i,r,l,c=(0,s.c)(8),{openSearch:u}=(0,h.useSearch)(),d=(0,p.useCommandModifierLabel)();return c[0]===Symbol.for("react.memo_cache_sentinel")?(e=(0,a.default)("group","flex","h-8","w-64","max-w-full","items-center","gap-x-2","rounded-lg","border","border-zinc-200","bg-white","px-2.5","text-[13px]","text-zinc-500","transition","hover:border-zinc-300","hover:bg-zinc-50","focus:outline-hidden","focus:ring-2","focus:ring-indigo-500/40","dark:border-zinc-700","dark:bg-zinc-900","dark:text-zinc-400","dark:hover:border-zinc-600","dark:hover:bg-zinc-800"),n=(0,t.jsx)(o.MagnifyingGlassIcon,{className:"h-3.5 w-3.5 shrink-0 text-zinc-400","aria-hidden":"true"}),i=(0,t.jsx)("span",{className:"flex-1 text-left",children:"Search..."}),c[0]=e,c[1]=n,c[2]=i):(e=c[0],n=c[1],i=c[2]),c[3]!==d?(r=null!=d?(0,t.jsxs)("kbd",{className:"inline-flex items-center gap-1 rounded-sm border border-zinc-200 bg-zinc-50 px-1 text-[11px] font-medium leading-4 text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500",children:[(0,t.jsx)("span",{className:"text-[12px] leading-none",children:d}),(0,t.jsx)("span",{children:"K"})]}):null,c[3]=d,c[4]=r):r=c[4],c[5]!==u||c[6]!==r?(l=(0,t.jsxs)("button",{type:"button",onClick:u,className:e,children:[n,i,r]}),c[5]=u,c[6]=r,c[7]=l):l=c[7],l},R=()=>{let e,n,i,r=(0,s.c)(4),{openSearch:l}=(0,h.useSearch)();return r[0]===Symbol.for("react.memo_cache_sentinel")?(e=(0,a.default)("inline-flex","items-center","justify-center","rounded-md","p-2","text-zinc-500","hover:bg-zinc-200/60","hover:text-zinc-900","dark:text-zinc-400","dark:hover:bg-zinc-800","dark:hover:text-zinc-100","lg:hidden"),r[0]=e):e=r[0],r[1]===Symbol.for("react.memo_cache_sentinel")?(n=(0,t.jsx)(o.MagnifyingGlassIcon,{className:"h-5 w-5","aria-hidden":"true"}),r[1]=n):n=r[1],r[2]!==l?(i=(0,t.jsx)("button",{type:"button",onClick:l,className:e,"aria-label":"Search docs",children:n}),r[2]=l,r[3]=i):i=r[3],i},C=()=>{let e,o,n,r,l=(0,s.c)(6),{onOpen:c}=(0,d.useSidebarContext)();return l[0]!==c?(e=()=>{c?.()},l[0]=c,l[1]=e):e=l[1],l[2]===Symbol.for("react.memo_cache_sentinel")?(o=(0,a.default)("inline-flex","items-center","justify-center","rounded-md","p-2","text-zinc-500","hover:bg-zinc-200/60","hover:text-zinc-900","dark:text-zinc-400","dark:hover:bg-zinc-800","dark:hover:text-zinc-100","lg:hidden"),l[2]=o):o=l[2],l[3]===Symbol.for("react.memo_cache_sentinel")?(n=(0,t.jsx)(i.Bars3Icon,{className:"h-5 w-5","aria-hidden":"true"}),l[3]=n):n=l[3],l[4]!==e?(r=(0,t.jsx)("button",{type:"button",onClick:e,className:o,"aria-label":"Open docs menu",children:n}),l[4]=e,l[5]=r):r=l[5],r},x=()=>{let e,o,n,i=(0,s.c)(6),c=(0,l.useRouter)();i[0]!==c.pathname?(e=T(c.pathname),i[0]=c.pathname,i[1]=e):e=i[1];let u=e;return i[2]!==u?(o=S.map(e=>{let s="changelog"===e.id?u:!u;return(0,t.jsxs)(r.default,{href:e.href,"aria-current":s?"page":void 0,className:(0,a.default)("relative","flex","h-full","items-center","px-3","text-[11px]","font-medium","tracking-[0.08em]","uppercase","transition-colors",s?"text-zinc-900 dark:text-zinc-100":"text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"),children:[e.name,s?(0,t.jsx)("span",{className:"absolute inset-x-2 bottom-0 h-0.5 bg-indigo-500","aria-hidden":"true"}):null]},e.id)}),i[2]=u,i[3]=o):o=i[3],i[4]!==o?(n=(0,t.jsx)("nav",{"aria-label":"Product areas",className:"hidden h-full items-stretch lg:flex",children:o}),i[4]=o,i[5]=n):n=i[5],n};e.s(["DocsHeader",0,()=>{let e,o,i,d,h,p,g,y,w,b,v,S,A,M,E=(0,s.c)(19),U=(0,l.useRouter)();E[0]!==U.pathname?(e=T(U.pathname),E[0]=U.pathname,E[1]=e):e=E[1];let L=e;E[2]!==U.pathname?(o=_(U.pathname),E[2]=U.pathname,E[3]=o):o=E[3];let P=o;return E[4]===Symbol.for("react.memo_cache_sentinel")?(i=(0,a.default)("sticky","top-0","z-30","p-0"),d=(0,a.default)("relative","flex",f.DOCS_TOP_BAR_HEIGHT_CLASS,"items-center","gap-2","border-b","border-zinc-200","bg-zinc-100","dark:border-zinc-800","dark:bg-zinc-950","px-3","sm:gap-3"),E[4]=i,E[5]=d):(i=E[4],d=E[5]),E[6]===Symbol.for("react.memo_cache_sentinel")?(h=(0,t.jsx)(C,{}
2),E[6]=h):h=E[6],E[7]===Symbol.for("react.memo_cache_sentinel")?(p=(0,t.jsx)(c.Image,{src:"/meticulous-wordmark-dark.svg",alt:"Meticulous",width:120,height:17,className:"h-[17px] w-auto dark:hidden",style:{width:"auto"}}),E[7]=p):p=E[7],E[8]===Symbol.for("react.memo_cache_sentinel")?(g=(0,t.jsxs)("div",{className:"flex h-full min-w-0 items-stretch gap-2 sm:gap-3",children:[h,(0,t.jsx)("div",{className:"flex shrink-0 items-center rounded-sm py-2 px-2.5",children:(0,t.jsxs)(r.default,{href:"/",className:"flex shrink-0 items-center","aria-label":"Go to Meticulous app",children:[p,(0,t.jsx)(c.Image,{src:"/meticulous-wordmark.svg",alt:"Meticulous",width:120,height:17,className:"hidden h-[17px] w-auto dark:block",style:{width:"auto"}})]})}),(0,t.jsx)(x,{})]}),E[8]=g):g=E[8],E[9]===Symbol.for("react.memo_cache_sentinel")?(y=(0,t.jsx)("div",{className:"pointer-events-none absolute inset-y-0 left-1/2 hidden -translate-x-1/2 items-center lg:flex",children:(0,t.jsx)("div",{className:"pointer-events-auto",children:(0,t.jsx)(I,{})})}),E[9]=y):y=E[9],E[10]===Symbol.for("react.memo_cache_sentinel")?(w=(0,t.jsx)(R,{}),b=(0,t.jsx)(m.DocsThemeToggle,{}),E[10]=w,E[11]=b):(w=E[10],b=E[11]),E[12]===Symbol.for("react.memo_cache_sentinel")?(v=(0,a.default)("group","hidden","items-center","gap-0.5","rounded-sm","px-1.5","py-1","text-xs","font-normal","text-zinc-500","transition-colors","hover:bg-zinc-200/70","hover:text-zinc-900","dark:text-zinc-400","dark:hover:bg-zinc-800","dark:hover:text-zinc-100","sm:inline-flex"),E[12]=v):v=E[12],E[13]===Symbol.for("react.memo_cache_sentinel")?(S=(0,t.jsxs)("div",{className:d,children:[g,y,(0,t.jsxs)("div",{className:"ml-auto flex shrink-0 flex-row items-center gap-x-2",children:[w,b,(0,t.jsxs)(r.default,{href:"/",className:v,children:["Go to app",(0,t.jsx)(n.ArrowUpRightIcon,{className:"h-4 w-4 shrink-0 text-zinc-400 opacity-0 transition-opacity group-hover:opacity-100","aria-hidden":!0})]}),(0,t.jsx)(u.AccountButton,{variant:"light"})]})]}),E[13]=S):S=E[13],E[14]!==P||E[15]!==L?(A=L?null:(0,t.jsx)("nav",{"aria-label":"Docs sections",className:(0,a.default)("hidden","h-9","items-stretch","gap-1","border-b","border-zinc-200","bg-white","dark:border-zinc-800","dark:bg-zinc-900","px-2","lg:flex","sm:px-4"),children:(0,t.jsx)("div",{className:"flex min-w-0 flex-1 items-stretch gap-1 overflow-x-auto scrollbar-hide",children:k.map(e=>{let s=P.id===e.id;return(0,t.jsxs)(r.default,{href:e.href,"aria-current":s?"page":void 0,className:(0,a.default)("relative","flex","shrink-0","items-center","px-3","text-[13px]","whitespace-nowrap","transition-colors","font-normal",s?"text-indigo-500 dark:text-indigo-400":"text-zinc-600 hover:text-indigo-900 dark:text-zinc-400 dark:hover:text-zinc-100"),children:[e.name,s?(0,t.jsx)("span",{className:"absolute inset-x-2 -bottom-px h-0.5 bg-indigo-500","aria-hidden":"true"}):null]},e.id)})})}),E[14]=P,E[15]=L,E[16]=A):A=E[16],E[17]!==A?(M=(0,t.jsxs)("header",{className:i,children:[S,A]}),E[17]=A,E[18]=M):M=E[18],M}],715524);var A=e.i(983888),M=e.i(244142),E=e.i(33468),U=e.i(687652);function L(e){return Math.max(e-1,0)}function P(e,s){if(!s.trim())return e;try{let o=RegExp(`(${s})`,"gi");return e.split(o).map((e,s)=>o.test(e)?(0,t.jsx)("mark",{className:"rounded-xs bg-indigo-500/15 text-indigo-500 dark:text-indigo-400",children:e},s):(0,t.jsx)("span",{children:e},s))}catch{return e}}e.s(["DocsSearch",0,()=>{let e,n,i,l,c,u,d,p,m,f,g,y,w,b,v,k,S,T,_,I,R,C,x=(0,s.c)(50),{query:O,results:N,isOpen:D,search:j,closeSearch:F}=(0,h.useSearch)(),[H,$]=(0,U.useState)(0),q=(0,U.useRef)(null);x[0]===Symbol.for("react.memo_cache_sentinel")?(e=[],x[0]=e):e=x[0];let B=(0,U.useRef)(e);x[1]!==D?(n=()=>{D&&(setTimeout(()=>{q.current?.focus()},50),$(0))},i=[D],x[1]=D,x[2]=n,x[3]=i):(n=x[2],i=x[3]),(0,U.useEffect)(n,i),x[4]!==F||x[5]!==D||x[6]!==N||x[7]!==H?(l=()=>{let e=e=>{D&&("ArrowDown"===e.key?(e.preventDefault(),$(e=>Math.min(e+1,N.length-1))):"ArrowUp"===e.key?(e.preventDefault(),$(L)):"Enter"===e.key&&N[H]&&(e.preventDefault(),window.location.href=N[H].url,F()))};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},c=[D,N,H,F],x[4]=F,x[5]=D,x[6]=N,x[7]=H,x[8]=l,x[9]=c):(l=x[8],c=x[9]),(0,U.useEffect)(l,c),x[10]===Symbol.for("react.memo_cache_sentinel")?(u=()=>{$(0)},x[10]=u):u=x[10],x[11]!==N?(d=[N],x[11]=N,x[12]=d):d=x[12],(0,U.useEffect)(u,d),x[13]!==H?(p=()=>{B.current[H]&&B.current[H]?.scrollIntoView({behavior:"smooth",block:"nearest"})}
2,m=[H],x[13]=H,x[14]=p,x[15]=m):(p=x[14],m=x[15]),(0,U.useEffect)(p,m),x[16]!==j?(f=e=>{j(e.target.value)},x[16]=j,x[17]=f):f=x[17];let G=f;return x[18]===Symbol.for("react.memo_cache_sentinel")?(g=(0,t.jsx)(M.Transition.Child,{as:U.Fragment,enter:"ease-out duration-300",enterFrom:"opacity-0",enterTo:"opacity-100",leave:"ease-in duration-200",leaveFrom:"opacity-100",leaveTo:"opacity-0",children:(0,t.jsx)("div",{className:"fixed inset-0 bg-zinc-900/40 transition-opacity dark:bg-zinc-950/60"})}),x[18]=g):g=x[18],x[19]===Symbol.for("react.memo_cache_sentinel")?(y=(0,t.jsx)(o.MagnifyingGlassIcon,{className:"pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-zinc-400","aria-hidden":"true"}),x[19]=y):y=x[19],x[20]!==G||x[21]!==O?(w=(0,t.jsx)("input",{ref:q,type:"text",className:"h-12 w-full border-0 bg-transparent pl-11 pr-4 text-zinc-900 placeholder:text-zinc-400 focus:ring-0 sm:text-sm dark:text-zinc-100 dark:placeholder:text-zinc-500",placeholder:"Search documentation...",value:O,onChange:G}),x[20]=G,x[21]=O,x[22]=w):w=x[22],x[23]===Symbol.for("react.memo_cache_sentinel")?(b=(0,t.jsx)(E.XMarkIcon,{className:"h-5 w-5","aria-hidden":"true"}),x[23]=b):b=x[23],x[24]!==F?(v=(0,t.jsx)("button",{type:"button",className:"absolute right-4 top-3.5 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300","aria-label":"Close search",onClick:F,children:b}),x[24]=F,x[25]=v):v=x[25],x[26]!==w||x[27]!==v?(k=(0,t.jsxs)("div",{className:"relative",children:[y,w,v]}),x[26]=w,x[27]=v,x[28]=k):k=x[28],x[29]!==F||x[30]!==O||x[31]!==N||x[32]!==H?(S=N.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("div",{className:"border-b border-zinc-200 px-4 py-2 dark:border-zinc-800",children:(0,t.jsxs)("p",{className:"text-xs text-zinc-500",children:[N.length," ",1===N.length?"result":"results"," found"]})}),(0,t.jsx)("ul",{className:"max-h-128 scroll-py-3 overflow-y-auto p-3",children:N.map((e,s)=>(0,t.jsx)("li",{ref:e=>{B.current[s]=e},children:(0,t.jsx)(r.default,{href:e.url,className:(0,a.default)("group flex cursor-pointer select-none rounded-md p-3",s===H?"bg-zinc-100 dark:bg-zinc-800":"hover:bg-zinc-50 dark:hover:bg-zinc-800/60"),onClick:F,onMouseEnter:()=>$(s),children:(0,t.jsxs)("div",{className:"flex-auto",children:[(0,t.jsx)("p",{className:"text-sm font-medium text-zinc-900 dark:text-zinc-100",children:P(e.title,O)}),(0,t.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:P(e.excerpt,O)})]})})},e.id))})]}),x[29]=F,x[30]=O,x[31]=N,x[32]=H,x[33]=S):S=x[33],x[34]!==O||x[35]!==N.length?(T=O&&0===N.length&&(0,t.jsxs)("div",{className:"px-6 py-14 text-center text-sm sm:px-14",children:[(0,t.jsxs)("p",{className:"text-zinc-500",children:["No results found for “",O,"”"]}),(0,t.jsx)("p",{className:"mt-2 text-zinc-400",children:"Try searching with different keywords"})]}),x[34]=O,x[35]=N.length,x[36]=T):T=x[36],x[37]!==O?(_=!O&&(0,t.jsxs)("div",{className:"px-6 py-14 text-center text-sm sm:px-14",children:[(0,t.jsx)(o.MagnifyingGlassIcon,{className:"mx-auto h-6 w-6 text-zinc-400","aria-hidden":"true"}),(0,t.jsx)("p",{className:"mt-4 text-zinc-500",children:"Search for documentation, guides, and troubleshooting tips"}),(0,t.jsxs)("p",{className:"mt-2 text-xs text-zinc-400",children:["Press"," ",(0,t.jsx)("kbd",{className:"rounded-sm border border-zinc-200 bg-zinc-50 px-1.5 py-0.5 text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-400",children:"Esc"})," ","to close"]})]}),x[37]=O,x[38]=_):_=x[38],x[39]!==k||x[40]!==S||x[41]!==T||x[42]!==_?(I=(0,t.jsx)("div",{className:"fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20",children:(0,t.jsx)(M.Transition.Child,{as:U.Fragment,enter:"ease-out duration-300",enterFrom:"opacity-0 scale-95",enterTo:"opacity-100 scale-100",leave:"ease-in duration-200",leaveFrom:"opacity-100 scale-100",leaveTo:"opacity-0 scale-95",children:(0,t.jsxs)(A.Dialog.Panel,{className:"mx-auto max-w-3xl transform divide-y divide-zinc-200 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-zinc-200 transition-all dark:divide-zinc-800 dark:bg-zinc-900 dark:ring-zinc-700",children:[k,S,T,_]})})}),x[39]=k,x[40]=S,x[41]=T,x[42]=_,x[43]=I):I=x[43],x[44]!==F||x[45]!==I?(R=(0,t.jsxs)(A.Dialog,{as:"div",className:"relative z-50","aria-label":"Search docs",onClose:F,children:[g,I]}),x[44]=F,x[45]=I,x[46]=R):R=x[46],x[47]!==D||x[48]!==R?(C=(0,t.jsx)(M.Transition.Root,{show:D,as:U.Fragment,children:R}),x[47]=D,x[48]=R,x[49]=C):C=x[49],C}],368054)},190923,831288,e=>{"use strict";let t=new Map,s=new Map;e.s(["docsSidebarExpandKey",0,(e,t)=>`${e}:${t}`,"getDocsSidebarScrollTop",0,e=>s.get(e)??0,"getDocsSidebarSectionExpanded",0,e=>t.get(e),"setDocsSidebarScrollTop",0,(e,t)=>{s.set(e,t)},"setDocsSidebarSectionExpanded",0,(e,s)=>{t.set(e,s)}],190923);var o=e.i(318008),n=e.i(35203),i=e.i(398145),a=e.i(628144),r=e.i(944967),l=e.i(326659);e.s(["DocsMobileProductNav",0,()=>{let e,t,s,c=(0,n.c)(6),u=(0,a.useRouter)();c[0]!==u.pathname?(e=(0,l.isDocsChangelogPath)(u.pathname),c[0]=u.pathname,c[1]=e):e=c[1];let d=e;return c[2]!==d?(t=l.DOCS_PRODUCT_AREAS.map(e=>{let t="changelog"===e.id?d:!d;return(0,o.jsx)("li",{children:(0,o.jsx)(i.default,{href:e.href,"aria-current":t?"page":void 0,className:(0,r.default)("block","rounded-md","px-3","py-1","text-[13px]","leading-5","font-normal",t?"text-indigo-500 dark:text-indigo-400":"text-zinc-600 hover:text-black dark:text-zinc-400 dark:hover:text-white"),children:e.name})},e.id)}),c[2]=d,c[3]=t):t=c[3],c[4]!==t?(s=(0,o.jsx)("ul",{role:"list",className:"space-y-0.5 px-2 lg:hidden",children:t}),c[4]=t,c[5]=s):s=c[5],s}],831288)},435919,552099,707740,73253,761947,917910,e=>{"use strict";let t,s,o,n,i;var a,r=e.i(318008),l=e.i(35203),c=e.i(687652);let u=(0,c.createContext)(null),d=e=>{let t,s,o,n,i,a,c=(0,l.c)(12);return c[0]!==e?({isOpen:t,onOpen:o,onClose:s,...n}=e,c[0]=e,c[1]=t,c[2]=s,c[3]=o,c[4]=n):(t=c[1],s=c[2],o=c[3],n=c[4]),c[5]!==t||c[6]!==s||c[7]!==o?(i={isOpen:t,onOpen:o,onClose:s}
2,c[5]=t,c[6]=s,c[7]=o,c[8]=i):i=c[8],c[9]!==n||c[10]!==i?(a=(0,r.jsx)(u.Provider,{value:i,...n}),c[9]=n,c[10]=i,c[11]=a):a=c[11],a},h=()=>{let e=(0,c.useContext)(u);if(!e)throw Error("useSidebarContext() must be used within a <SidebarLayout> component");return e};e.s(["SidebarContextProvider",0,d,"useSidebarContext",0,h],552099),e.s(["SidebarLayout",0,e=>{let t,s,o,n,i=(0,l.c)(6),{children:a}=e;i[0]===Symbol.for("react.memo_cache_sentinel")?(t={isOpen:!1},i[0]=t):t=i[0];let[u,h]=(0,c.useState)(t),{isOpen:p}=u;i[1]===Symbol.for("react.memo_cache_sentinel")?(s=()=>{h({isOpen:!0})},i[1]=s):s=i[1];let m=s;i[2]===Symbol.for("react.memo_cache_sentinel")?(o=()=>{h({isOpen:!1})},i[2]=o):o=i[2];let f=o;return i[3]!==a||i[4]!==p?(n=(0,r.jsx)(d,{isOpen:p,onOpen:m,onClose:f,children:a}),i[3]=a,i[4]=p,i[5]=n):n=i[5],n}],435919);let p=c.forwardRef(function({title:e,titleId:t,...s},o){return c.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:o,"aria-labelledby":t},s),e?c.createElement("title",{id:t},e):null,c.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"}))});e.s(["Bars3Icon",0,p],707740);var m=e.i(944967);e.s(["SidebarMain",0,e=>{let t,s,o,n,i=(0,l.c)(15),{children:a,className:c,desktopPaddingClassName:u,mobileHeaderStartContent:d,mobileHeaderEndContent:f,mobileHeaderPaddingRightClassName:g,hideMobileHeader:y}=e,w=void 0===u?"md:pl-64":u,b=void 0===g?"pr-1 sm:pr-3":g,v=void 0!==y&&y,{onOpen:k}=h();i[0]!==k?(t=()=>{k?.()},i[0]=k,i[1]=t):t=i[1];let S=t;return i[2]!==c||i[3]!==w?(s=(0,m.default)(w,"flex","flex-col","flex-1","h-full",c),i[2]=c,i[3]=w,i[4]=s):s=i[4],i[5]!==S||i[6]!==v||i[7]!==f||i[8]!==b||i[9]!==d?(o=v?null:(0,r.jsxs)("div",{className:(0,m.default)("sticky","top-0","z-10","md:hidden","flex","items-center","bg-white","border-b-[0.5px]","border-zinc-200","pl-1","pt-1","sm:pl-3","sm:pt-3",b),children:[(0,r.jsxs)("button",{type:"button",className:(0,m.default)("-ml-0.5","-mt-0.5","h-12","w-12","inline-flex","items-center","justify-center","rounded-md","text-zinc-700","hover:text-zinc-500","focus:outline-hidden","focus:ring-2","focus:ring-inset","focus:ring-indigo-500"),onClick:S,children:[(0,r.jsx)("span",{className:"sr-only",children:"Open sidebar"}),(0,r.jsx)(p,{className:(0,m.default)("h-6","w-6"),"aria-hidden":"true"})]}),d,(0,r.jsx)("div",{className:(0,m.default)("flex-1")}),f]}),i[5]=S,i[6]=v,i[7]=f,i[8]=b,i[9]=d,i[10]=o):o=i[10],i[11]!==a||i[12]!==s||i[13]!==o?(n=(0,r.jsxs)("div",{className:s,children:[o,a]}),i[11]=a,i[12]=s,i[13]=o,i[14]=n):n=i[14],n}],73253);var f={get url(){return e.F("node_modules/.pnpm/[email protected]/node_modules/flexsearch/dist/flexsearch.bundle.module.min.mjs")},env:{DEV:!1,PROD:!0,MODE:"production",BASE_URL:"/",SSR:!1}};function g(e,t,s){let o=typeof s,n=typeof e;if("undefined"!==o){if("undefined"!==n){if(s){if("function"===n&&o===n)return function(t){return e(s(t))};if((t=e.constructor)===s.constructor){if(t===Array)return s.concat(e);if(t===Map){var i=new Map(s);for(var a of e)i.set(a[0],a[1]);return i}
2if(t===Set){for(i of(a=new Set(s),e.values()))a.add(i);return a}}}return e}return s}return"undefined"===n?t:e}function y(e,t){return void 0===e?t:e}function w(){return Object.create(null)}function b(e){return"string"==typeof e}function v(e){return"object"==typeof e}function k(e,t){if(b(t))e=e[t];else for(let s=0;e&&s<t.length;s++)e=e[t[s]];return e}let S=/[^\p{L}\p{N}]+/u,T=/(\d{3})/g,_=/(\D)(\d{3})/g,I=/(\d{3})(\D)/g,R=/[\u0300-\u036f]/g;function C(e={}){if(!this||this.constructor!==C)return new C(...arguments);if(arguments.length)for(e=0;e<arguments.length;e++)this.assign(arguments[e]);else this.assign(e)}function x(e){e.F=null,e.B.clear(),e.D.clear()}function A(e,t,s){s||(t||"object"!=typeof e?"object"==typeof t&&(s=t,t=0):s=e),s&&(e=s.query||e,t=s.limit||t);let o=""+(t||0);s&&(o+=(s.offset||0)+!!s.context+!!s.suggest+(!1!==s.resolve)+(s.resolution||this.resolution)+(s.boost||0)),e=(""+e).toLowerCase(),this.cache||(this.cache=new M);let n=this.cache.get(e+o);if(!n){let i=s&&s.cache;i&&(s.cache=!1),n=this.search(e,t,s),i&&(s.cache=i),this.cache.set(e+o,n)}return n}function M(e){this.limit=e&&!0!==e?e:1e3,this.cache=new Map,this.h=""}(a=C.prototype).assign=function(e){this.normalize=g(e.normalize,!0,this.normalize);let t=e.include,s=t||e.exclude||e.split,o;if(s||""===s){if("object"==typeof s&&s.constructor!==RegExp){let e="";o=!t,t||(e+="\\p{Z}"),s.letter&&(e+="\\p{L}"),s.number&&(e+="\\p{N}",o=!!t),s.symbol&&(e+="\\p{S}"),s.punctuation&&(e+="\\p{P}
2"),s.control&&(e+="\\p{C}"),(s=s.char)&&(e+="object"==typeof s?s.join(""):s);try{this.split=RegExp("["+(t?"^":"")+e+"]+","u")}catch(e){this.split=/\s+/}}else this.split=s,o=!1===s||"a1a".split(s).length<2;this.numeric=g(e.numeric,o)}else{try{this.split=g(this.split,S)}catch(e){this.split=/\s+/}this.numeric=g(e.numeric,g(this.numeric,!0))}if(this.prepare=g(e.prepare,null,this.prepare),this.finalize=g(e.finalize,null,this.finalize),s=e.filter,this.filter="function"==typeof s?s:g(s&&new Set(s),null,this.filter),this.dedupe=g(e.dedupe,!0,this.dedupe),this.matcher=g((s=e.matcher)&&new Map(s),null,this.matcher),this.mapper=g((s=e.mapper)&&new Map(s),null,this.mapper),this.stemmer=g((s=e.stemmer)&&new Map(s),null,this.stemmer),this.replacer=g(e.replacer,null,this.replacer),this.minlength=g(e.minlength,1,this.minlength),this.maxlength=g(e.maxlength,1024,this.maxlength),this.rtl=g(e.rtl,!1,this.rtl),(this.cache=s=g(e.cache,!0,this.cache))&&(this.F=null,this.L="number"==typeof s?s:2e5,this.B=new Map,this.D=new Map,this.I=this.H=128),this.h="",this.J=null,this.A="",this.K=null,this.matcher)for(let e of this.matcher.keys())this.h+=(this.h?"|":"")+e;
2if(this.stemmer)for(let e of this.stemmer.keys())this.A+=(this.A?"|":"")+e;return this},a.addStemmer=function(e,t){return this.stemmer||(this.stemmer=new Map),this.stemmer.set(e,t),this.A+=(this.A?"|":"")+e,this.K=null,this.cache&&x(this),this},a.addFilter=function(e){return"function"==typeof e?this.filter=e:(this.filter||(this.filter=new Set),this.filter.add(e)),this.cache&&x(this),this},a.addMapper=function(e,t){return"object"==typeof e?this.addReplacer(e,t):e.length>1?this.addMatcher(e,t):(this.mapper||(this.mapper=new Map),this.mapper.set(e,t),this.cache&&x(this),this)},a.addMatcher=function(e,t){return"object"==typeof e?this.addReplacer(e,t):e.length<2&&(this.dedupe||this.mapper)?this.addMapper(e,t):(this.matcher||(this.matcher=new Map),this.matcher.set(e,t),this.h+=(this.h?"|":"")+e,this.J=null,this.cache&&x(this),this)},a.addReplacer=function(e,t){return"string"==typeof e?this.addMatcher(e,t):(this.replacer||(this.replacer=[]),this.replacer.push(e,t),this.cache&&x(this),this)},a.encode=function(e,t){if(this.cache&&e.length<=this.H)if(this.F){if(this.B.has(e))return this.B.get(e)}else this.F=setTimeout(x,50,this);this.normalize&&(e="function"==typeof this.normalize?this.normalize(e):e.normalize("NFKD").replace(R,"").toLowerCase()),this.prepare&&(e=this.prepare(e)),this.numeric&&e.length>3&&(e=e.replace(_,"$1 $2").replace(I,"$1 $2").replace(T,"$1 "));let s=!(this.dedupe||this.mapper||this.filter||this.matcher||this.stemmer||this.replacer),o=[],n=w(),i,a,r=this.split||""===this.split?e.split(this.split):[e];for(let e=0,c,u;e<r.length;e++)if((c=u=r[e])&&!(c.length<this.minlength||c.length>this.maxlength)){if(t){if(n[c])continue;n[c]=1}else{if(i===c)continue;i=c}if(s)o.push(c);else if(!this.filter||("function"==typeof this.filter?this.filter(c):!this.filter.has(c))){if(this.cache&&c.length<=this.I)if(this.F){var l=this.D.get(c);if(l||""===l){l&&o.push(l);continue}}else this.F=setTimeout(x,50,this);if(this.stemmer){let e;for(this.K||(this.K=RegExp("(?!^)("+this.A+")$"));e!==c&&c.length>2;)e=c,c=c.replace(this.K,e=>this.stemmer.get(e))}if(c&&(this.mapper||this.dedupe&&c.length>1)){l="";for(let e=0,t="",s,o;e<c.length;e++)(s=c.charAt(e))===t&&this.dedupe||((o=this.mapper&&this.mapper.get(s))||""===o?(o!==t||!this.dedupe)&&(t=o)&&(l+=o):l+=t=s);c=l}if(this.matcher&&c.length>1&&(this.J||(this.J=RegExp("("+this.h+")","g")),c=c.replace(this.J,e=>this.matcher.get(e))),c&&this.replacer)for(l=0;c&&l<this.replacer.length;l+=2)c=c.replace(this.replacer[l],this.replacer[l+1]);if(this.cache&&u.length<=this.I&&(this.D.set(u,c),this.D.size>this.L&&(this.D.clear(),this.I=this.I/1.1|0)),c){if(c!==u)if(t){if(n[c])continue;n[c]=1}else{if(a===c)continue;a=c}o.push(c)}}}return this.finalize&&(o=this.finalize(o)||o),this.cache&&e.length<=this.H&&(this.B.set(e,o),this.B.size>this.L&&(this.B.clear(),this.H=this.H/1.1|0)),o},M.prototype.set=function(e,t){this.cache.set(this.h=e,t),this.cache.size>this.limit&&this.cache.delete(this.cache.keys().next().value)},M.prototype.get=function(e){let t=this.cache.get(e);return t&&this.h!==e&&(this.cache.delete(e),this.cache.set(this.h=e,t)),t},M.prototype.remove=function(e){for(let t of this.cache){let s=t[0];t[1].includes(e)&&this.cache.delete(s)}},M.prototype.clear=function(){this.cache.clear(),this.h=""};let E={normalize:!1,numeric:!1,dedupe:!1},U={},L=new Map([["b","p"],["v","f"],["w","f"],["z","s"],["x","s"],["d","t"],["n","m"],["c","k"],["g","k"],["j","k"],["q","k"],["i","e"],["y","e"],["u","o"]]),P=new Map([["ae","a"],["oe","o"],["sh","s"],["kh","k"],["th","t"],["ph","f"],["pf","f"]]),O=[/([^aeo])h(.)/g,"$1$2",/([aeo])h([^aeo]|$)/g,"$1$2",/(.)\1+/g,"$1"],N={a:"",e:"",i:"",o:"",u:"",y:"",b:1,f:1,p:1,v:1,c:2,g:2,j:2,k:2,q:2,s:2,x:2,z:2,ß:2,d:3,t:3,l:4,m:5,n:5,r:6};var D={Exact:E,Default:U,Normalize:U,LatinBalance:{mapper:L},LatinAdvanced:{mapper:L,matcher:P,replacer:O},LatinExtra:{mapper:L,replacer:O.concat([/(?!^)[aeo]/g,""]),matcher:P},LatinSoundex:{dedupe:!1,include:{letter:!0},finalize:function(e){for(let s=0;s<e.length;s++){var t=e[s];let o=t.charAt(0),n=N[o];for(let e=1,s;e<t.length&&("h"===(s=t.charAt(e))||"w"===s||!(s=N[s])||s===n||(o+=s,n=s,4!==o.length));e++);e[s]=o}}},CJK:{split:""},LatinExact:E,LatinDefault:U,LatinSimple:U};function j(e,t,s,o){let n=[];for(let i=0,a;i<e.index.length;i++)if(t>=(a=e.index[i]).length)t-=a.length;else{let i=(t=a[o?"splice":"slice"](t,s)).length;if(i&&(n=n.length?n.concat(t):t,s-=i,o&&(e.length-=i),!s))break;t=0}return n}function F(e){if(!this||this.constructor!==F)return new F(e);this.index=e?[e]:[],this.length=e?e.length:0;let t=this;return new Proxy([],{get:(e,s)=>"length"===s?t.length:"push"===s?function(e){t.index[t.index.length-1].push(e),t.length++}:"pop"===s?function(){if(t.length)return t.length--,t.index[t.index.length-1].pop()}:"indexOf"===s?function(e){let s=0;for(let o=0,n,i;o<t.index.length;o++){if((i=(n=t.index[o]).indexOf(e))>=0)return s+i;s+=n.length}return -1}:"includes"===s?function(e){for(let s=0;s<t.index.length;s++)if(t.index[s].includes(e))return!0;return!1}
2:"slice"===s?function(e,s){return j(t,e||0,s||t.length,!1)}:"splice"===s?function(e,s){return j(t,e||0,s||t.length,!0)}:"constructor"===s?Array:"symbol"!=typeof s?(e=t.index[s/0x80000000|0])&&e[s]:void 0,set:(e,s,o)=>(e=s/0x80000000|0,(t.index[e]||(t.index[e]=[]))[s]=o,t.length++,!0)})}function H(e=8){if(!this||this.constructor!==H)return new H(e);this.index=w(),this.h=[],this.size=0,e>32?(this.B=B,this.A=BigInt(e)):(this.B=q,this.A=e)}function $(e=8){if(!this||this.constructor!==$)return new $(e);this.index=w(),this.h=[],this.size=0,e>32?(this.B=B,this.A=BigInt(e)):(this.B=q,this.A=e)}function q(e){let t=2**this.A-1;if("number"==typeof e)return e&t;let s=0,o=this.A+1;for(let n=0;n<e.length;n++)s=(s*o^e.charCodeAt(n))&t;return 32===this.A?s+0x80000000:s}function B(e){let t=BigInt(2)**this.A-BigInt(1);var s=typeof e;if("bigint"===s)return e&t;if("number"===s)return BigInt(e)&t;s=BigInt(0);let o=this.A+BigInt(1);for(let n=0;n<e.length;n++)s=(s*o^BigInt(e.charCodeAt(n)))&t;return s}async function G(e){var o=(e=e.data).task;let n=e.id,i=e.args;if("init"===o)s=e.options||{},(o=e.factory)?(Function("return "+o)()(self),t=new self.FlexSearch.Index(s),delete self.FlexSearch):t=new eE(s),postMessage({id:n});else{let a;"export"===o&&(i[1]?(i[0]=s.export,i[2]=0,i[3]=1):i=null),"import"===o?i[0]&&(e=await s.import.call(t,i[0]),t.import(i[0],e)):((a=i&&t[o].apply(t,i))&&a.then&&(a=await a),a&&a.await&&(a=await a.await),"search"===o&&a.result&&(a=a.result)),postMessage("search"===o?{id:n,msg:a}:{id:n})}}function W(e){V.call(e,"add"),V.call(e,"append"),V.call(e,"search"),V.call(e,"update"),V.call(e,"remove"),V.call(e,"searchCache")}function z(){o=i=0}function V(e){this[e+"Async"]=function(){let t,s=arguments;var a=s[s.length-1];if("function"==typeof a&&(t=a,delete s[s.length-1]),o?i||(i=Date.now()-n>=this.priority*this.priority*3):(o=setTimeout(z,0),n=Date.now()),i){let t=this;return new Promise(o=>{setTimeout(function(){o(t[e+"Async"].apply(t,s))},0)})}let r=this[e].apply(this,s);return a=r.then?r:new Promise(e=>e(r)),t&&a.then(t),a}}F.prototype.clear=function(){this.index.length=0},F.prototype.push=function(){},H.prototype.get=function(e){let t=this.index[this.B(e)];return t&&t.get(e)},H.prototype.set=function(e,t){var s=this.B(e);let o=this.index[s];o?(s=o.size,o.set(e,t),(s-=o.size)&&this.size++):(this.index[s]=o=new Map([[e,t]]),this.h.push(o),this.size++)},$.prototype.add=function(e){var t=this.B(e);let s=this.index[t];s?(t=s.size,s.add(e),(t-=s.size)&&this.size++):(this.index[t]=s=new Set([e]),this.h.push(s),this.size++)},(a=H.prototype).has=$.prototype.has=function(e){let t=this.index[this.B(e)];return t&&t.has(e)},a.delete=$.prototype.delete=function(e){let t=this.index[this.B(e)];t&&t.delete(e)&&this.size--},a.clear=$.prototype.clear=function(){this.index=w(),this.h=[],this.size=0},a.values=$.prototype.values=function*(){for(let e=0;e<this.h.length;e++)for(let t of this.h[e].values())yield t},a.keys=$.prototype.keys=function*(){for(let e=0;e<this.h.length;e++)for(let t of this.h[e].keys())yield t},a.entries=$.prototype.entries=function*(){for(let e=0;e<this.h.length;e++)for(let t of this.h[e].entries())yield t};let K=0;function Y(e={},t){var s,o,n;function i(s){function o(e){let t=(e=e.data||e).id,s=t&&l.h[t];s&&(s(e.msg),delete l.h[t])}if(this.worker=s,this.h=w(),this.worker)return(r?this.worker.on("message",o):this.worker.onmessage=o,e.config)?new Promise(function(t){K>1e9&&(K=0),l.h[++K]=function(){t(l)},l.worker.postMessage({id:K,task:"init",factory:a,options:e})}):(this.priority=e.priority||4,this.encoder=t||null,this.worker.postMessage({task:"init",factory:a,options:e}),this)}if(!this||this.constructor!==Y)return new Y(e);let a="u">typeof self?self._factory:"u">typeof window?window._factory:null;a&&(a=a.toString());let r="u"<typeof window,l=this,c=(s=a,o=r,n=e.worker,o?Promise.resolve({}).then(function(e){return new e.Worker(f.dirname+"/node/node.mjs")}):s?new window.Worker(URL.createObjectURL(new Blob(["onmessage="+G.toString()],{type:"text/javascript"}))):new window.Worker("string"==typeof n?n:f.url.replace("/worker.js","/worker/wor
2ker.js").replace("flexsearch.bundle.module.min.js","module/worker/worker.js").replace("flexsearch.bundle.module.min.mjs","module/worker/worker.js"),{type:"module"}));return c.then?c.then(function(e){return i.call(l,e)}):i.call(this,c)}function J(e){Y.prototype[e]=function(){let t,s=this,o=[].slice.call(arguments);var n=o[o.length-1];return"function"==typeof n&&(t=n,o.pop()),n=new Promise(function(t){"export"===e&&"function"==typeof o[0]&&(o[0]=null),K>1e9&&(K=0),s.h[++K]=t,s.worker.postMessage({task:e,id:K,args:o})}),t?(n.then(t),this):n}}function X(e,t,s,o){if(!e.length)return e;if(1===e.length)return e=e[0],e=s||e.length>t?e.slice(s,s+t):e,o?ed.call(this,e):e;let n=[];for(let i=0,a,r;i<e.length;i++)if((a=e[i])&&(r=a.length)){if(s){if(s>=r){s-=r;continue}r=(a=a.slice(s,s+t)).length,s=0}if(r>t&&(a=a.slice(0,t),r=t),!n.length&&r>=t)return o?ed.call(this,a):a;if(n.push(a),!(t-=r))break}return n=n.length>1?[].concat.apply([],n):n[0],o?ed.call(this,n):n}function Q(e,t,s,o){var n=o[0];if(n[0]&&n[0].query)return e[t].apply(e,n);if(!("and"!==t&&"not"!==t||e.result.length||e.await||n.suggest))return o.length>1&&(n=o[o.length-1]),(o=n.resolve)?e.await||e.result:e;let i=[],a=0,r=0,l,c,u,d,h;for(t=0;t<o.length;t++)if(n=o[t]){var p=void 0;if(n.constructor===ei)p=n.await||n.result;else if(n.then||n.constructor===Array)p=n;else{a=n.limit||0,r=n.offset||0,u=n.suggest,c=n.resolve,l=((d=n.highlight||e.highlight)||n.enrich)&&c,p=n.queue;let s=n.async||p,o=n.index,m=n.query;if(o?e.index||(e.index=o):o=e.index,m||n.tag){let a=n.field||n.pluck;if(a&&(m&&(!e.query||d)&&(e.query=m,e.field=a,e.highlight=d),o=o.index.get(a)),p&&(h||e.await)){let a;h=1;let r=e.C.length,l=new Promise(function(e){a=e});!function(t,o){l.h=function(){o.index=null,o.resolve=!1;let n=s?t.searchAsync(o):t.search(o);return n.then?n.then(function(t){return e.C[r]=t=t.result||t,a(t),t}):(n=n.result||n,a(n),n)}}(o,Object.assign({},n)),e.C.push(l),i[t]=l;continue}n.resolve=!1,n.index=null,p=s?o.searchAsync(n):o.search(n),n.resolve=c,n.index=o}else if(n.and)p=Z(n,"and",o);else if(n.or)p=Z(n,"or",o);else if(n.not)p=Z(n,"not",o);else{if(!n.xor)continue;p=Z(n,"xor",o)}}p.await?(h=1,p=p.await):p.then?(h=1,p=p.then(function(e){return e.result||e})):p=p.result||p,i[t]=p}if(h&&!e.await&&(e.await=new Promise(function(t){e.return=t})),h){let t=Promise.all(i).then(function(o){for(let n=0;n<e.C.length;n++)if(e.C[n]===t){e.C[n]=function(){return s.call(e,o,a,r,l,c,u,d)};break}ea(e)});e.C.push(t)}else{if(!e.await)return s.call(e,i,a,r,l,c,u,d);e.C.push(function(){return s.call(e,i,a,r,l,c,u,d)})}return c?e.await||e.result:e}function Z(e,t,s){let o=(e=e[t])[0]||e;return o.index||(o.index=s),s=new ei(o),e.length>1&&(s=s[t].apply(s,e.slice(1))),s}function ee(e,t,s,o,n,i,a){return e.length&&(this.result.length&&e.push(this.result),e.length<2?this.result=e[0]:(this.result=el(e,t,s,!1,this.h),s=0)),n&&(this.await=null),n?this.resolve(t,s,o,a):this}function et(e,t,s,o,n,i,a){let r;if(!i&&!this.result.length)return n?this.result:this;if(e.length)if(this.result.length&&e.unshift(this.result),e.length<2)this.result=e[0];else{let o=0;for(let t=0,s,n;t<e.length;t++)if((s=e[t])&&(n=s.length))o<n&&(o=n);else if(!i){o=0;break}o?(this.result=er(e,o,t,s,i,this.h,n),r=!0):this.result=[]}else i||(this.result=e);return n&&(this.await=null),n?this.resolve(t,s,o,a,r):this}function es(e,t,s,o,n,i,a){if(e.length)if(this.result.length&&e.unshift(this.result),e.length<2)this.result=e[0];else{e:{i=s;var r=this.h;let o=[],a=w(),l=0;for(let t=0,s;t<e.length;t++)if(s=e[t]){l<s.length&&(l=s.length);for(let e=0,t;e<s.length;e++)if(t=s[e])for(let e=0,s;e<t.length;e++)a[s=t[e]]=a[s]?2:1}for(let s=0,c,u=0;s<l;s++)for(let l=0,d;l<e.length;l++)if((d=e[l])&&(c=d[s])){for(let d=0,h;d<c.length;d++)if(1===a[h=c[d]])if(i)i--;else if(n){if(o.push(h),o.length===t){e=o;break e}}else{let n=s+(l?r:0);if(o[n]||(o[n]=[]),o[n].push(h),++u===t){e=o;break e}}}e=o}this.result=e,r=!0}else i||(this.result=e);return n&&(this.await=null),n?this.resolve(t,s,o,a,r):this}function eo(e,t,s,o,n,i,a){if(!i&&!this.result.length)return n?this.result:this;if(e.length&&this.result.length){e:{i=s;var r=[];e=new Set(e.flat().flat());for(let s=0,o,a=0;s<this.result.length;s++)if(o=this.result[s]){for(let l=0,c;l<o.length;
2l++)if(c=o[l],!e.has(c)){if(i)i--;else if(n){if(r.push(c),r.length===t){e=r;break e}}else if(r[s]||(r[s]=[]),r[s].push(c),++a===t){e=r;break e}}}e=r}this.result=e,r=!0}return n&&(this.await=null),n?this.resolve(t,s,o,a,r):this}function en(e,t,s,o,n){let i,a,r,l,c;"string"==typeof n?(i=n,n=""):i=n.template,a=i.indexOf("$1"),r=i.substring(a+2),a=i.substring(0,a);let u=n&&n.boundary,d=!n||!1!==n.clip,h=n&&n.merge&&r&&a&&RegExp(r+" "+a,"g");n=n&&n.ellipsis;var p=0;if("object"==typeof n){var m=n.template;p=m.length-2,n=n.pattern}"string"!=typeof n&&(n=!1===n?"":"..."),p&&(n=m.replace("$1",n)),m=n.length-p,"object"==typeof u&&(0===(l=u.before)&&(l=-1),0===(c=u.after)&&(c=-1),u=u.total||9e5),p=new Map;for(let P=0,O,N;P<t.length;P++){let D;if(o)D=t,N=o;else{var f=t[P];if(!(N=f.field))continue;D=f.result}O=s.get(N).encoder,"string"!=typeof(f=p.get(O))&&(f=O.encode(e),p.set(O,f));for(let e=0;e<D.length;e++){var g=D[e].doc;if(!g||!(g=k(g,N)))continue;var y=g.trim().split(/\s+/);if(!y.length)continue;g="";var w=[];let t=[];for(var b=-1,v=-1,S=0,T=0;T<y.length;T++){let e;var _=y[T],I=O.encode(_);if((I=I.length>1?I.join(" "):I[0])&&_){for(var R=_.length,C=(O.split?_.replace(O.split,""):_).length-I.length,x="",A=0,M=0;M<f.length;M++){var E=f[M];if(E){var U=E.length;U+=C<0?0:C,A&&U<=A||(E=I.indexOf(E))>-1&&(x=(E?_.substring(0,E):"")+a+_.substring(E,E+U)+r+(E+U<R?_.substring(E+U):""),A=U,e=!0)}}x&&(u&&(b<0&&(b=g.length+ +!!g),v=g.length+ +!!g+x.length,S+=R,t.push(w.length),w.push({match:x})),g+=(g?" ":"")+x)}if(e){if(u&&S>=u)break}else _=y[T],g+=(g?" ":"")+_,u&&w.push({text:_})}if(S=t.length*(i.length-2),l||c||u&&g.length-S>u)if(S=u+S-2*m,T=v-b,l>0&&(T+=l),c>0&&(T+=c),T<=S)y=l?b-(l>0?l:0):b-((S-T)/2|0),w=c?v+(c>0?c:0):y+S,d||(y>0&&" "!==g.charAt(y)&&" "!==g.charAt(y-1)&&(y=g.indexOf(" ",y))<0&&(y=0),w<g.length&&" "!==g.charAt(w-1)&&" "!==g.charAt(w)&&((w=g.lastIndexOf(" ",w))<v?w=v:++w)),g=(y?n:"")+g.substring(y,w)+(w<g.length?n:"");else{for(v=[],b={},S={},T={},_={},I={},x=C=R=0,M=A=1;;){var L=void 0;for(let e=0,s;e<t.length;e++){if(s=t[e],x)if(C!==x){if(T[e+1])continue;if(b[s+=x]){R-=m,S[e+1]=1,T[e+1]=1;continue}if(s>=w.length-1){if(s>=w.length){T[e+1]=1,s>=y.length&&(S[e+1]=1);continue}R-=m}if(g=w[s].text,U=c&&I[e])if(U>0){if(g.length>U)if(T[e+1]=1,!d)continue;else g=g.substring(0,U);(U-=g.length)||(U=-1),I[e]=U}else{T[e+1]=1;continue}if(R+g.length+1<=u)g=" "+g,v[e]+=g;else if(d)(L=u-R-1)>0&&(g=" "+g.substring(0,L),v[e]+=g),T[e+1]=1;else{T[e+1]=1;continue}}else{if(T[e])continue;if(b[s-=C]){R-=m,T[e]=1,S[e]=1;continue}if(s<=0){if(s<0){T[e]=1,S[e]=1;continue}R-=m}if(g=w[s].text,U=l&&_[e])if(U>0){if(g.length>U)if(T[e]=1,!d)continue;else g=g.substring(g.length-U);(U-=g.length)||(U=-1),_[e]=U}else{T[e]=1;continue}if(R+g.length+1<=u)g+=" ",v[e]=g+v[e];else if(d)(L=g.length+1-(u-R))>
2=0&&L<g.length&&(g=g.substring(L)+" ",v[e]=g+v[e]),T[e]=1;else{T[e]=1;continue}}else{let t;if(g=w[s].match,l&&(_[e]=l),c&&(I[e]=c),e&&R++,s?!e&&m&&(R+=m):(S[e]=1,T[e]=1),s>=y.length-1||s<w.length-1&&w[s+1].match?t=1:m&&(R+=m),R-=i.length-2,!e||R+g.length<=u)v[e]=g;else{L=A=M=S[e]=0;break}t&&(S[e+1]=1,T[e+1]=1)}R+=g.length,L=b[s]=1}if(L)C===x?x++:C++;else{if(C===x?A=0:M=0,!A&&!M)break;A?x=++C:x++}}g="";for(let e=0;e<v.length;e++)g+=(S[e]?e?" ":"":(e&&!n?" ":"")+n)+v[e];n&&!S[v.length]&&(g+=n)}h&&(g=g.replace(h," ")),D[e].highlight=g}if(o)break}return t}function ei(e,t){if(!this||this.constructor!==ei)return new ei(e,t);let s=0,o,n,i,a,r,l;if(e&&e.index){let o=e;if(t=o.index,s=o.boost||0,n=o.query){i=o.field||o.pluck,a=o.highlight;let s=o.resolve;e=o.async||o.queue,o.resolve=!1,o.index=null,e=e?t.searchAsync(o):t.search(o),o.resolve=s,o.index=t,e=e.result||e}else e=[]}if(e&&e.then){let t=this;o=[e=e.then(function(e){t.C[0]=t.result=e.result||e,ea(t)})],e=[],r=new Promise(function(e){l=e})}this.index=t||null,this.result=e||[],this.h=s,this.C=o||[],this.await=r||null,this.return=l||null,this.highlight=a||null,this.query=n||"",this.field=i||""}function ea(e,t){let s=e.result;var o=e.await;e.await=null;for(let t=0,n;t<e.C.length;t++)if(n=e.C[t]){if("function"==typeof n)s=n(),e.C[t]=s=s.result||s,t--;else if(n.h)s=n.h(),e.C[t]=s=s.result||s,t--;else if(n.then)return e.await=o}return o=e.return,e.C=[],e.return=null,t||o(s),s}function er(e,t,s,o,n,i,a){let r=e.length,l=[],c,u;c=w();for(let d=0,h,p,m,f;d<t;d++)for(let t=0;t<r;t++)if(d<(m=e[t]).length&&(h=m[d]))for(let e=0;e<h.length;e++){if((u=c[p=h[e]])?c[p]++:(u=0,c[p]=1),f=l[u]||(l[u]=[]),!a){let e=d+(t||!n?0:i||0);f=f[e]||(f[e]=[])}if(f.push(p),a&&s&&u===r-1&&f.length-o===s)return o?f.slice(o):f}if(e=l.length)if(n)l=l.length>1?el(l,s,o,a,i):(l=l[0])&&s&&l.length>s||o?l.slice(o,s+o):l;else{if(e<r)return[];
2if(l=l[e-1],s||o)if(a)(l.length>s||o)&&(l=l.slice(o,s+o));else{n=[];for(let e=0,t;e<l.length;e++)if(t=l[e]){if(o&&t.length>o)o-=t.length;else if((s&&t.length>s||o)&&(t=t.slice(o,s+o),s-=t.length,o&&(o-=t.length)),n.push(t),!s)break}l=n}}return l}function el(e,t,s,o,n){let i,a,r=[],l=w();var c=e.length;if(o){for(n=c-1;n>=0;n--)if(a=(o=e[n])&&o.length){for(c=0;c<a;c++)if(!l[i=o[c]]){if(l[i]=1,s)s--;else if(r.push(i),r.length===t)return r}}}else for(let u=c-1,d,h=0;u>=0;u--){d=e[u];for(let e=0;e<d.length;e++)if(a=(o=d[e])&&o.length){for(let d=0;d<a;d++)if(!l[i=o[d]])if(l[i]=1,s)s--;else{let s=(e+(u<c-1&&n||0))/(u+1)|0;if((r[s]||(r[s]=[])).push(i),++h===t)return r}}}return r}function ec(e){let t=[],s=w(),o=w();for(let n=0,i,a,r,l,c,u,d;n<e.length;n++){a=(i=e[n]).field,r=i.result;for(let e=0;e<r.length;e++)"object"!=typeof(c=r[e])?c={id:l=c}:l=c.id,(u=s[l])?u.push(a):(c.field=s[l]=[a],t.push(c)),(d=c.highlight)&&((u=o[l])||(o[l]=u={},c.highlight=u),u[a]=d)}return t}function eu(e,t,s,o,n){return(e=this.tag.get(e))&&(e=e.get(t))?((t=e.length-o)>0&&((s&&t>s||o)&&(e=e.slice(o,o+s)),n&&(e=ed.call(this,e))),e):[]}function ed(e){if(!this||!this.store)return e;if(this.db)return this.index.get(this.field[0]).db.enrich(e);let t=Array(e.length);for(let s=0,o;s<e.length;s++)o=e[s],t[s]={id:o,doc:this.store.get(o)};return t}function eh(e){let t,s;if(!this||this.constructor!==eh)return new eh(e);let o=e.document||e.doc||e;if(this.B=[],this.field=[],this.D=[],this.key=(t=o.key||o.id)&&em(t,this.D)||"id",(s=e.keystore||0)&&(this.keystore=s),this.fastupdate=!!e.fastupdate,this.reg=!this.fastupdate||e.worker||e.db?s?new $(s):new Set:s?new H(s):new Map,this.h=(t=o.store||null)&&t&&!0!==t&&[],this.store=t?s?new H(s):new Map:null,this.cache=(t=e.cache||null)&&new M(t),e.cache=!1,this.worker=e.worker||!1,this.priority=e.priority||4,this.index=ep.call(this,e,o),this.tag=null,(t=o.tag)&&("string"==typeof t&&(t=[t]),t.length)){this.tag=new Map,this.A=[],this.F=[];for(let e=0,s,o;e<t.length;e++){if(!(o=(s=t[e]).field||s))throw Error("The tag field from the document descriptor is undefined.");s.custom?this.A[e]=s.custom:(this.A[e]=em(o,this.D),s.filter&&("string"==typeof this.A[e]&&(this.A[e]=new String(this.A[e])),this.A[e].G=s.filter)),this.F[e]=o,this.tag.set(o,new Map)}}if(this.worker){for(let t of(this.fastupdate=!1,e=[],this.index.values()))t.then&&e.push(t);if(e.length){let t=this;return Promise.all(e).then(function(e){let s=0;for(let o of t.index.entries()){let n=o[0],i=o[1];i.then&&(i=e[s],t.index.set(n,i),s++)}return t})}}else e.db&&(this.fastupdate=!1,this.mount(e.db))}function ep(e,t){let s=new Map,o=t.index||t.field||t;b(o)&&(o=[o]);for(let t=0,i,a;t<o.length;t++){if(b(i=o[t])||(a=i,i=i.field),a=v(a)?Object.assign({},e,a):e,this.worker){var n=void 0;n=(n=a.encoder)&&n.encode?n:new C("string"==typeof n?D[n]:n||{}),n=new Y(a,n),s.set(i,n)}this.worker||s.set(i,new eE(a,this.reg)),a.custom?this.B[t]=a.custom:(this.B[t]=em(i,this.D),a.filter&&("string"==typeof this.B[t]&&(this.B[t]=new String(this.B[t])),this.B[t].G=a.filter)),this.field[t]=i}if(this.h){b(e=t.store)&&(e=[e]);for(let t=0,s,o;t<e.length;t++)o=(s=e[t]).field||s,s.custom?(this.h[t]=s.custom,s.custom.O=o):(this.h[t]=em(o,this.D),s.filter&&("string"==typeof this.h[t]&&(this.h[t]=new String(this.h[t])),this.h[t].G=s.filter))}return s}function em(e,t){let s=e.split(":"),o=0;for(let n=0;n<s.length;n++)"]"===(e=s[n])[e.length-1]&&(e=e.substring(0,e.length-2))&&(t[o]=!0),e&&(s[o++]=e);return o<s.length&&(s.length=o),o>1?s:s[0]}function ef(e,t=0){let s=[],o=[];for(let n of(t&&(t=25e4/t*5e3|0),e.entries()))o.push(n),o.length===t&&(s.push(o),o=[]);return o.length&&s.push(o),s}function eg(e,t){t||(t=new Map);for(let s=0,o;s<e.length;s++)o=e[s],t.set(o[0],o[1]);return t}function ey(e,t=0){let s=[],o=[];for(let n of(t&&(t=25e4/t*1e3|0),e.entries()))o.push([n[0],ef(n[1])[0]||[]]),o.length===t&&(s.push(o),o=[]);return o.length&&s.push(o),s}function ew(e,t){t||(t=new Map);for(let s=0,o,n;s<e.length;s++)o=e[s],n=t.get(o[0]),t.set(o[0],eg(o[1],n));return t}function eb(e){let t=[],s=[];for(let o of e.keys())s.push(o),25e4===s.length&&(t.push(s),s=[]);return s.length&&t.push(s),t}function ev(e,t){t||(t=new Set);for(let s=0;s<e.length;s++)t.add(e[s]);return t}function ek(e,t,s,o,n,i,a=0){let r=o&&o.constructor===Array;var l=r?o.shift():o;if(!l)return this.export(e,t,n,i+1);if((l=e((t?t+".":"")+(a+1)+"."+s,JSON.stringify(l)))&&l.then){let c=this;return l.then(function(){return ek.call(c,e,t,s,r?o:null,n,i,a+1)})}return ek.call(this,e,t,s,r?o:null,n,i,a+1)}function eS(e,t){let s="";for(let o of e.entries()){e=o[0];let n=o[1],i="";for(let e=0,s;e<n.length;e++){s=n[e]||[""];let o="";for(let e=0;e<s.length;e++)o+=(o?",":"")+("string"===t?'"'+s[e]+'"':s[e]);o="["+o+"]",i+=(i?",":"")+o}i='["'+e+'",['+i+"]]",s+=(s?",":"")+i}return s}function eT(e,t){let s=0;var o=void 0===t;if(e.constructor===Array){for(let n=0,i,a,r;n<e.length;n++)if((i=e[n])&&i.length){if(o)return 1;if((a=i.indexOf(t))>=0){if(i.length>1)return i.splice(a,1),1;if(delete e[n],s)return 1;r=1}else{if(r)return 1;s++}}}else for(let n of e.entries())o=n[0],eT(n[1],t)?s++:e.delete(o);return s}J("add"),J("append"),J("search"),J("update"),J("remove"),J("clear"),J("export"),J("import"),Y.prototype.searchCache=A,W(Y.prototype),eh.prototype.add=function(e,t,s){if(v(e)&&(e=k(t=e,this.key)),t&&(e||0===e)){if(!s&&this.reg.has(e))return this.update(e,t);for(let i=0,a;i<this.field.length;i++){a=this.B[i];var o=this.index.get(this.field[i]);if("function"==typeof a){var n=a(t);n&&o.add(e,n,s,!0)}
2else(!(n=a.G)||n(t))&&(a.constructor===String?a=[""+a]:b(a)&&(a=[a]),function e(t,s,o,n,i,a,r,l){if(t=t[r])if(n===s.length-1){if(t.constructor===Array){if(o[n]){for(s=0;s<t.length;s++)i.add(a,t[s],!0,!0);return}t=t.join(" ")}i.add(a,t,l,!0)}else if(t.constructor===Array)for(r=0;r<t.length;r++)e(t,s,o,n,i,a,r,l);else r=s[++n],e(t,s,o,n,i,a,r,l)}(t,a,this.D,0,o,e,a[0],s))}if(this.tag)for(o=0;o<this.A.length;o++){var i=this.A[o];n=this.tag.get(this.F[o]);let r=w();if("function"==typeof i){if(!(i=i(t)))continue}else{var a=i.G;if(a&&!a(t))continue;i.constructor===String&&(i=""+i),i=k(t,i)}if(n&&i){b(i)&&(i=[i]);for(let t=0,o,l;t<i.length;t++)if(!r[o=i[t]]&&(r[o]=1,(a=n.get(o))?l=a:n.set(o,l=[]),!s||!l.includes(e))){if(l.length===0x80000000-1){if(a=new F(l),this.fastupdate)for(let e of this.reg.values())e.includes(l)&&(e[e.indexOf(l)]=a);n.set(o,l=a)}l.push(e),this.fastupdate&&((a=this.reg.get(e))?a.push(l):this.reg.set(e,[l]))}}}if(this.store&&(!s||!this.store.has(e))){let o;if(this.h){o=w();for(let e=0,n;e<this.h.length;e++){let i;if(!(s=(n=this.h[e]).G)||s(t)){if("function"==typeof n){if(!(i=n(t)))continue;n=[n.O]}else if(b(n)||n.constructor===String){o[n]=t[n];continue}!function e(t,s,o,n,i,a){if(t=t[i],n===o.length-1)s[i]=a||t;else if(t)if(t.constructor===Array)for(s=s[i]=Array(t.length),i=0;i<t.length;i++)e(t,s,o,n,i);else s=s[i]||(s[i]=w()),i=o[++n],e(t,s,o,n,i)}(t,o,n,0,n[0],i)}}}this.store.set(e,o||t)}this.worker&&(this.fastupdate||this.reg.add(e))}return this},ei.prototype.or=function(){return Q(this,"or",ee,arguments)},ei.prototype.and=function(){return Q(this,"and",et,arguments)},ei.prototype.xor=function(){return Q(this,"xor",es,arguments)},ei.prototype.not=function(){return Q(this,"not",eo,arguments)},(a=ei.prototype).limit=function(e){if(this.await){let t=this;this.C.push(function(){return t.limit(e).result})}else if(this.result.length){let t=[];for(let s=0,o;s<this.result.length;s++)if(o=this.result[s])if(o.length<=e){if(t[s]=o,!(e-=o.length))break}else{t[s]=o.slice(0,e);break}this.result=t}return this},a.offset=function(e){if(this.await){let t=this;this.C.push(function(){return t.offset(e).result})}else if(this.result.length){let t=[];for(let s=0,o;s<this.result.length;s++)(o=this.result[s])&&(o.length<=e?e-=o.length:(t[s]=o.slice(e),e=0));this.result=t}return this},a.boost=function(e){if(this.await){let t=this;this.C.push(function(){return t.boost(e).result})}else this.h+=e;return this},a.resolve=function(e,t,s,o,n){let i=this.await?ea(this,!0):this.result;if(i.then){let a=this;return i.then(function(){return a.resolve(e,t,s,o,n)})}return i.length&&("object"==typeof e?(s=!!(o=e.highlight||this.highlight)||e.enrich,t=e.offset,e=e.limit):s=!!(o=o||this.highlight)||s,i=n?s?ed.call(this.index,i):i:X.call(this.index,i,e||100,t,s)),this.finalize(i,o)},a.finalize=function(e,t){if(e.then){let s=this;return e.then(function(e){return s.finalize(e,t)})}t&&e.length&&this.query&&(e=en(this.query,e,this.index.index,this.field,t));let s=this.return;return this.highlight=this.index=this.result=this.C=this.await=this.return=null,this.query=this.field="",s&&s(e),e},w(),eh.prototype.search=function(e,t,s,o){let n,i,a,r,l,c,u;s||(!t&&v(e)?(s=e,e=""):v(t)&&(s=t,t=0));let d=[];var h=[];let p=0,m=!0,f;if(s){s.constructor===Array&&(s={index:s}),e=s.query||e,n=s.pluck,i=s.merge,r=s.boost,c=n||s.field||(c=s.index)&&(c.index?null:c);var g=this.tag&&s.tag;a=s.suggest,m=!1!==s.resolve,l=s.cache;var k=!!(f=m&&this.store&&s.highlight)||m&&this.store&&s.enrich;t=s.limit||t;var S=s.offset||0;if(t||(t=100*!!m),g&&(!this.db||!o)){g.constructor!==Array&&(g=[g]);var T=[];for(let e=0,t;e<g.length;e++)if((t=g[e]).field&&t.tag){var _=t.tag;if(_.constructor===Array)for(var I=0;I<_.length;I++)T.push(t.field,_[I]);else T.push(t.field,_)}else{_=Object.keys(t);for(let e=0,s,o;e<_.length;e++)if((o=t[s=_[e]]).constructor===Array)for(I=0;I<o.length;I++)T.push(s,o[I]);else T.push(s,o)}if(g=T,!e){if(h=[],T.length)for(g=0;g<T.length;g+=2){if(this.db){if(!(o=this.index.get(T[g])))continue;h.push(o=o.db.tag(T[g+1],t,S,k))}else o=eu.call(this,T[g],T[g+1],t,S,k);d.push(m?{field:T[g],tag:T[g+1],result:o}:[o])}if(h.length){let e=this;return Promise.all(h).then(function(t){for(let e=0;e<t.length;e++)m?d[e].result=t[e]:d[e]=t[e];return m?d:new ei(d.length>1?er(d,1,0,0,a,r):d[0],e)})}return m?d:new ei(d.length>1?er(d,1,0,0,a,r):d[0],this)}}!m&&!n&&(c=c||this.field)&&(b(c)?n=c:(c.constructor===Array&&1===c.length&&(c=c[0]),n=c.field||
2c.index)),c&&c.constructor!==Array&&(c=[c])}c||(c=this.field),T=(this.worker||this.db)&&!o&&[];for(let n=0,i,r,v;n<c.length;n++){let C;if(r=c[n],!this.db||!this.tag||this.B[n]){if(b(r)||(r=(C=r).field,e=C.query||e,t=y(C.limit,t),S=y(C.offset,S),a=y(C.suggest,a),k=!!(f=m&&this.store&&y(C.highlight,f))||m&&this.store&&y(C.enrich,k),l=y(C.cache,l)),o)i=o[n];else{I=(_=C||s||{}).enrich;var R=this.index.get(r);if(g&&(this.db&&(_.tag=g,_.field=c,u=R.db.support_tag_search),!u&&I&&(_.enrich=!1),u||(_.limit=0,_.offset=0)),i=l?R.searchCache(e,g&&!u?0:t,_):R.search(e,g&&!u?0:t,_),g&&!u&&(_.limit=t,_.offset=S),I&&(_.enrich=I),T){T[n]=i;continue}}if(v=(i=i.result||i)&&i.length,g&&v){if(_=[],I=0,this.db&&o){if(!u)for(R=c.length;R<o.length;R++){let e=o[R];if(e&&e.length)I++,_.push(e);else if(!a)return m?d:new ei(d,this)}}else for(let e=0,t;e<g.length;e+=2){if(!(t=this.tag.get(g[e])))if(a)continue;else return m?d:new ei(d,this);if((t=t&&t.get(g[e+1]))&&t.length)I++,_.push(t);else if(!a)return m?d:new ei(d,this)}if(I){if(!(v=(i=function(e,t,s,o,n){let i=w(),a=[];for(let e=0,s;e<t.length;e++){s=t[e];for(let e=0;e<s.length;e++)i[s[e]]=1}if(n){for(let t=0,n;t<e.length;t++)if(i[n=e[t]]){if(o)o--;else if(a.push(n),i[n]=0,s&&0==--s)break}}else for(let s=0,o,n;s<e.result.length;s++)for(o=e.result[s],t=0;t<o.length;t++)i[n=o[t]]&&((a[s]||(a[s]=[])).push(n),i[n]=0);return a}(i,_,t,S,m)).length)&&!a)return m?i:new ei(i,this);I--}}if(v)h[p]=r,d.push(i),p++;else if(1===c.length)return m?d:new ei(d,this)}}if(T){if(this.db&&g&&g.length&&!u)for(k=0;k<g.length;k+=2){if(!(h=this.index.get(g[k])))if(a)continue;else return m?d:new ei(d,this);T.push(h.db.tag(g[k+1],t,S,!1))}let o=this;return Promise.all(T).then(function(n){return s&&(s.resolve=m),n.length&&(n=o.search(e,t,s,n)),n})}if(!p)return m?d:new ei(d,this);if(n&&(!k||!this.store))return d=d[0],m?d:new ei(d,this);for(S=0,T=[];S<h.length;S++){if(g=d[S],k&&g.length&&void 0===g[0].doc&&(this.db?T.push(g=this.index.get(this.field[0]).db.enrich(g)):g=ed.call(this,g)),n)return m?f?en(e,g,this.index,n,f):g:new ei(g,this);d[S]={field:h[S],result:g}}if(k&&this.db&&T.length){let t=this;return Promise.all(T).then(function(s){for(let e=0;e<s.length;e++)d[e].result=s[e];return f&&(d=en(e,d,t.index,n,f)),i?ec(d):d})}return f&&(d=en(e,d,this.index,n,f)),i?ec(d):d},(a=eh.prototype).mount=function(e){let t=this.field;if(this.tag)for(let e=0,o;e<this.F.length;e++){o=this.F[e];var s=void 0;this.index.set(o,s=new eE({},this.reg)),t===this.field&&(t=t.slice(0)),t.push(o),s.tag=this.tag.get(o)}s=[];let o={db:e.db,type:e.type,fastupdate:e.fastupdate};for(let n=0,i,a;n<t.length;n++){o.field=a=t[n],i=this.index.get(a);let r=new e.constructor(e.id,o);r.id=e.id,s[n]=r.mount(i),i.document=!0,n?i.bypass=!0:i.store=this.store}let n=this;return this.db=Promise.all(s).then(function(){n.db=!0})},a.commit=async function(){let e=[];for(let t of this.index.values())e.push(t.commit());await Promise.all(e),this.reg.clear()},a.destroy=function(){let e=[];for(let t of this.index.values())e.push(t.destroy());return Promise.all(e)},a.append=function(e,t){return this.add(e,t,!0)},a.update=function(e,t){return this.remove(e).add(e,t)},a.remove=function(e){for(var t of(v(e)&&(e=k(e,this.key)),this.index.values()))t.remove(e,!0);if(this.reg.has(e)){if(this.tag&&!this.fastupdate)for(let s of this.tag.values())for(let o of s){t=o[0];let n=o[1],i=n.indexOf(e);i>-1&&(n.length>1?n.splice(i,1):s.delete(t))}this.store&&this.store.delete(e),this.reg.delete(e)}return this.cache&&this.cache.remove(e),this},a.clear=function(){let e=[];for(let t of this.index.values()){let s=t.clear();s.then&&e.push(s)}if(this.tag)for(let e of this.tag.values())e.clear();return this.store&&this.store.clear(),this.cache&&this.cache.clear(),e.length?Promise.all(e):this},a.contain=function(e){return this.db?this.index.get(this.field[0]).db.has(e):this.reg.has(e)},a.cleanup=function(){for(let e of this.index.values())e.cleanup();return this},a.get=function(e){return this.db?this.index.get(this.field[0]).db.enrich(e).then(function(e){return e[0]&&e[0].doc||null}):this.store.get(e)||null},a.set=function(e,t){return"object"==typeof e&&(e=k(t=e,this.key)),this.store.set(e,t),this},a.searchCache=A,a.export=function(e,t,s=0,o=0){let n,i;if(s<this.field.length){let n=this.field[s];if((t=this.index.get(n).export(e,n,s,o=1))&&t.then){let o=this;return t.then(function(){return o.export(e,n,s+1)})}return this.export(e,n,s+1)}switch(o){case 0:n="reg",i=eb(this.reg),t=null;break;case 1:n="tag",i=this.tag&&ey(this.tag,this.reg.size),t=null;break;case 2:n="doc",i=this.store&&ef(this.store),t=null;break;default:return}return ek.call(this,e,t,n,i||null,s,o)},a.import=function(e,t){var s=e.split(".");"json"===s[s.length-1]&&s.pop();let o=s.length>2?s[0]:"";if(s=s.length>2?s[2]:s[1],this.worker&&o)return this.index.get(o).import(e);if(t){if("string"==typeof t&&(t=JSON.parse(t)),o)return this.index.get(o).import(s,t);switch(s){case"reg":this.fastupdate=!1,this.reg=ev(t,this.reg);for(let e=0,t;e<this.field.length;e++)(t=this.index.get(this.field[e])).fastupdate=!1,t.reg=this.reg;
2if(this.worker){for(let s of(t=[],this.index.values()))t.push(s.import(e));return Promise.all(t)}break;case"tag":this.tag=ew(t,this.tag);break;case"doc":this.store=eg(t,this.store)}}},W(eh.prototype),eE.prototype.remove=function(e,t){let s=this.reg.size&&(this.fastupdate?this.reg.get(e):this.reg.has(e));if(s){if(this.fastupdate){for(let t=0,o,n;t<s.length;t++)if((o=s[t])&&(n=o.length))if(o[n-1]===e)o.pop();else{let t=o.indexOf(e);t>=0&&o.splice(t,1)}}else eT(this.map,e),this.depth&&eT(this.ctx,e);t||this.reg.delete(e)}return this.db&&(this.commit_task.push({del:e}),this.M&&eU(this)),this.cache&&this.cache.remove(e),this};let e_={memory:{resolution:1},performance:{resolution:3,fastupdate:!0,context:{depth:1,resolution:1}},match:{tokenize:"forward"},score:{resolution:9,context:{depth:2,resolution:3}}};function eI(e,t,s,o,n,i,a){let r,l;if(!(r=t[s])||a&&!r[a]){if(a?((t=r||(t[s]=w()))[a]=1,(r=(l=e.ctx).get(a))?l=r:l.set(a,l=e.keystore?new H(e.keystore):new Map)):(l=e.map,t[s]=1),(r=l.get(s))?l=r:l.set(s,l=r=[]),i){for(let s=0,i;s<r.length;s++)if((i=r[s])&&i.includes(n)){if(s<=o)return;i.splice(i.indexOf(n),1),e.fastupdate&&(t=e.reg.get(n))&&t.splice(t.indexOf(i),1);break}}if((l=l[o]||(l[o]=[])).push(n),l.length===0x80000000-1){if(t=new F(l),e.fastupdate)for(let s of e.reg.values())s.includes(l)&&(s[s.indexOf(l)]=t);r[o]=l=t}e.fastupdate&&((o=e.reg.get(n))?o.push(l):e.reg.set(n,[l]))}}function eR(e,t,s,o,n){return s&&e>1?t+(o||0)<=e?s+(n||0):(e-1)/(t+(o||0))*(s+(n||0))+1|0:0}function eC(e,t,s,o,n,i,a){let r=e.length,l=e;if(r>1)l=er(e,t,s,o,n,i,a);else if(1===r)return a?X.call(null,e[0],s,o):new ei(e[0],this);return a?l:new ei(l,this)}function ex(e,t,s,o,n,i,a){return e=eM(this,e,t,s,o,n,i,a),this.db?e.then(function(e){return n?e||[]:new ei(e,this)}):e&&e.length?n?X.call(this,e,s,o):new ei(e,this):n?[]:new ei([],this)}function eA(e,t,s,o){let n=[];if(e&&e.length){if(e.length<=o)return void t.push(e);for(let t=0,s;t<o;t++)(s=e[t])&&(n[t]=s);if(n.length)return void t.push(n)}if(!s)return n}function eM(e,t,s,o,n,i,a,r){let l;return(s&&(l=e.bidirectional&&t>s)&&(l=s,s=t,t=l),e.db)?e.db.get(t,s,o,n,i,a,r):e=s?(e=e.ctx.get(s))&&e.get(t):e.map.get(t)}function eE(e,t){if(!this||this.constructor!==eE)return new eE(e);if(e){var s=b(e)?e:e.preset;s&&(e=Object.assign({},e_[s],e))}else e={};let o=!0===(s=e.context)?{depth:1}:s||{},n=b(e.encoder)?D[e.encoder]:e.encode||e.encoder||{};this.encoder=n.encode?n:"object"==typeof n?new C(n):{encode:n},this.resolution=e.resolution||9,this.tokenize=s=(s=e.tokenize)&&"default"!==s&&"exact"!==s&&s||"strict",this.depth="strict"===s&&o.depth||0,this.bidirectional=!1!==o.bidirectional,this.fastupdate=!!e.fastupdate,this.score=e.score||null,(s=e.keystore||0)&&(this.keystore=s),this.map=s?new H(s):new Map,this.ctx=s?new H(s):new Map,this.reg=t||(this.fastupdate?s?new H(s):new Map:s?new $(s):new Set),this.N=o.resolution||3,this.rtl=n.rtl||e.rtl||!1,this.cache=(s=e.cache||null)&&new M(s),this.resolve=!1!==e.resolve,(s=e.db)&&(this.db=this.mount(s)),this.M=!1!==e.commit,this.commit_task=[],this.commit_timer=null,this.priority=e.priority||4}function eU(e){e.commit_timer||(e.commit_timer=setTimeout(function(){e.commit_timer=null,e.db.commit(e)},1))}eE.prototype.add=function(e,t,s,o){if(t&&(e||0===e)){if(!o&&!s&&this.reg.has(e))return this.update(e,t);o=this.depth;let c=(t=this.encoder.encode(t,!o)).length;if(c){let u=w(),d=w(),h=this.resolution;for(let p=0;p<c;p++){let m=t[this.rtl?c-1-p:p];var n=m.length;if(n&&(o||!d[m])){var i=this.score?this.score(t,m,p,null,0):eR(h,c,p),a="";switch(this.tokenize){case"tolerant":if(eI(this,d,m,i,e,s),n>2){for(let t=1,o,r,l,c;t<n-1;t++)o=m.charAt(t),r=m.charAt(t+1),eI(this,d,a=(l=m.substring(0,t)+r)+o+(c=m.substring(t+2)),i,e,s),eI(this,d,a=l+c,i,e,s);eI(this,d,m.substring(0,m.length-1),i,e,s)}break;case"full":if(n>2){for(let o=0,l;o<n;o++)for(i=n;i>o;i--){a=m.substring(o,i),l=this.rtl?n-1-o:o;var r=this.score?this.score(t,m,p,a,l):eR(h,c,p,n,l);eI(this,d,a,r,e,s)}break}case"bidirectional":case"reverse":if(n>1){for(r=n-1;r>0;r--){a=m[this.rtl?n-1-r:r]+a;var l=this.score?this.score(t,m,p,a,r):eR(h,c,p,n,r);eI(this,d,a,l,e,s)}a=""}case"forward":if(n>1){for(r=0;r<n;r++)eI(this,d,a+=m[this.rtl?n-1-r:r],i,e,s);break}default:if(eI(this,d,m,i,e,s),o&&c>1&&p<c-1)for(n=this.N,a=m,i=Math.min(o+1,this.rtl?p+1:c-p),r=1;r<i;r++){m=t[this.rtl?c-1-p-r:p+r],l=this.bidirectional&&m>a;
2let o=this.score?this.score(t,a,p,m,r-1):eR(n+(c/2>n?0:1),c,p,i-1,r-1);eI(this,u,l?a:m,o,e,s,l?m:a)}}}}this.fastupdate||this.reg.add(e)}}return this.db&&(this.commit_task.push(s?{ins:e}:{del:e}),this.M&&eU(this)),this},eE.prototype.search=function(e,t,s){if(s||(t||"object"!=typeof e?"object"==typeof t&&(s=t,t=0):(s=e,e="")),s&&s.cache)return s.cache=!1,e=this.searchCache(e,t,s),s.cache=!0,e;let o=[],n,i,a,r=0,l,c,u,d,h;s&&(e=s.query||e,t=s.limit||t,r=s.offset||0,i=s.context,a=s.suggest,h=(l=s.resolve)&&s.enrich,u=s.boost,d=s.resolution,c=this.db&&s.tag),void 0===l&&(l=this.resolve),i=this.depth&&!1!==i;let p=this.encoder.encode(e,!i);if(n=p.length,t=t||100*!!l,1===n)return ex.call(this,p[0],"",t,r,l,h,c);if(2===n&&i&&!a)return ex.call(this,p[1],p[0],t,r,l,h,c);let m=w(),f=0,g;if(i&&(g=p[0],f=1),d||0===d||(d=g?this.N:this.resolution),this.db){if(this.db.search&&!1!==(s=this.db.search(this,p,t,r,a,l,h,c)))return s;let e=this;return async function(){for(let t,s;f<n;f++){if((s=p[f])&&!m[s]){if(m[s]=1,t=eA(t=await eM(e,s,g,0,0,!1,!1),o,a,d)){o=t;break}g&&(a&&t&&o.length||(g=s))}a&&g&&f===n-1&&!o.length&&(d=e.resolution,g="",f=-1,m=w())}return eC(o,d,t,r,a,u,l)}()}for(let e,t;f<n;f++){if((t=p[f])&&!m[t]){if(m[t]=1,e=eA(e=eM(this,t,g,0,0,!1,!1),o,a,d)){o=e;break}g&&(a&&e&&o.length||(g=t))}a&&g&&f===n-1&&!o.length&&(d=this.resolution,g="",f=-1,m=w())}return eC(o,d,t,r,a,u,l)},(a=eE.prototype).mount=function(e){return this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null),e.mount(this)},a.commit=function(){return this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null),this.db.commit(this)},a.destroy=function(){return this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null),this.db.destroy()},a.clear=function(){return this.map.clear(),this.ctx.clear(),this.reg.clear(),this.cache&&this.cache.clear(),this.db?(this.commit_timer&&clearTimeout(this.commit_timer),this.commit_timer=null,this.commit_task=[],this.db.clear()):this},a.append=function(e,t){return this.add(e,t,!0)},a.contain=function(e){return this.db?this.db.has(e):this.reg.has(e)},a.update=function(e,t){let s=this,o=this.remove(e);return o&&o.then?o.then(()=>s.add(e,t)):this.add(e,t)},a.cleanup=function(){return this.fastupdate&&(eT(this.map),this.depth&&eT(this.ctx)),this},a.searchCache=A,a.export=function(e,t,s=0,o=0){let n,i;switch(o){case 0:n="reg",i=eb(this.reg);break;case 1:n="cfg",i=null;break;case 2:n="map",i=ef(this.map,this.reg.size);break;case 3:n="ctx",i=ey(this.ctx,this.reg.size);break;default:return}return ek.call(this,e,t,n,i,s,o)},a.import=function(e,t){if(t)switch("string"==typeof t&&(t=JSON.parse(t)),"json"===(e=e.split("."))[e.length-1]&&e.pop(),3===e.length&&e.shift(),e=e.length>1?e[1]:e[0]){case"reg":this.fastupdate=!1,this.reg=ev(t,this.reg);break;case"map":this.map=eg(t,this.map);break;case"ctx":this.ctx=ew(t,this.ctx)}},a.serialize=function(e=!0){let t="",s="",o="";if(this.reg.size){let e;for(var n of this.reg.keys())e||(e=typeof n),t+=(t?",":"")+("string"===e?'"'+n+'"':n);for(let i of(t="index.reg=new Set(["+t+"]);",s="index.map=new Map(["+(s=eS(this.map,e))+"]);",this.ctx.entries())){n=i[0];let t=eS(i[1],e);t='["'+n+'",'+(t="new Map(["+t+"])")+"]",o+=(o?",":"")+t}o="index.ctx=new Map(["+o+"]);"}return e?"function inject(index){"+t+s+o+"}":t+s+o},W(eE.prototype);let eL="u">typeof window&&(window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB),eP=["map","ctx","tag","reg","cfg"],eO=w();function eN(e,t={}){if(!this||this.constructor!==eN)return new eN(e,t);"object"==typeof e&&(t=e,e=e.name),e||console.info("Default storage space was used, because a name was not passed."),this.id="flexsearch"+(e?":"+e.toLowerCase().replace(/[^a-z0-9_\-]/g,""):""),this.field=t.field?t.field.toLowerCase().replace(/[^a-z0-9_\-]/g,""):"",this.type=t.type,this.fastupdate=this.support_tag_search=!1,this.db=null,this.h={}}function eD(e,t,s){let o=e.value,n,i=0;for(let e=0,a;e<o.length;e++){if(a=s?o:o[e]){for(let s=0,i,r;s<t.length;s++)if(r=t[s],(i=a.indexOf(r))>=0)if(n=1,a.length>1)a.splice(i,1);else{o[e]=[];break}i+=a.length}if(s)break}i?n&&e.update(o):e.delete(),e.continue()}function ej(e,t){return new Promise((s,o)=>{e.onsuccess=e.oncomplete=function(){t&&t(this.result),t=null,s(this.result)},e.onerror=e.onblocked=o,e=null})}(a=eN.prototype).mount=function(e){return e.index?e.mount(this):(e.db=this,this.open())},a.open=function(){if(this.db)return this.db;let e=this;navigator.storage&&navigator.storage.persist&&navigator.storage.persist(),eO[e.id]||(eO[e.id]=[]),eO[e.id].push(e.field);let t=eL.open(e.id,1);return t.onupgradeneeded=function(){let t=e.db=this.result;for(let s=0,o;s<eP.length;s++){o=eP[s];for(let s=0,n;s<eO[e.id].length;s++)n=eO[e.id][s],t.objectStoreNames.contains(o+("reg"!==o&&n?":"+n:""))||t.createObjectStore(o+("reg"!==o&&n?":"+n:""))}},e.db=ej(t,function(t){e.db=t,e.db.onversionchange=function(){e.close()}})},a.close=function(){this.db&&this.db.close(),this.db=null}
2,a.destroy=function(){return ej(eL.deleteDatabase(this.id))},a.clear=function(){let e=[];for(let t=0,s;t<eP.length;t++){s=eP[t];for(let t=0,o;t<eO[this.id].length;t++)o=eO[this.id][t],e.push(s+("reg"!==s&&o?":"+o:""))}let t=this.db.transaction(e,"readwrite");for(let s=0;s<e.length;s++)t.objectStore(e[s]).clear();return ej(t)},a.get=function(e,t,s=0,o=0,n=!0,i=!1){e=this.db.transaction((t?"ctx":"map")+(this.field?":"+this.field:""),"readonly").objectStore((t?"ctx":"map")+(this.field?":"+this.field:"")).get(t?t+":"+e:e);let a=this;return ej(e).then(function(e){let t=[];if(!e||!e.length)return t;if(n){if(!s&&!o&&1===e.length)return e[0];for(let n=0,i;n<e.length;n++)if((i=e[n])&&i.length){if(o>=i.length){o-=i.length;continue}let e=s?o+Math.min(i.length-o,s):i.length;for(let s=o;s<e;s++)t.push(i[s]);if(o=0,t.length===s)break}return i?a.enrich(t):t}return e})},a.tag=function(e,t=0,s=0,o=!1){e=this.db.transaction("tag"+(this.field?":"+this.field:""),"readonly").objectStore("tag"+(this.field?":"+this.field:"")).get(e);let n=this;return ej(e).then(function(e){return e&&e.length&&!(s>=e.length)?t||s?(e=e.slice(s,s+t),o?n.enrich(e):e):e:[]})},a.enrich=function(e){"object"!=typeof e&&(e=[e]);let t=this.db.transaction("reg","readonly").objectStore("reg"),s=[];for(let o=0;o<e.length;o++)s[o]=ej(t.get(e[o]));return Promise.all(s).then(function(t){for(let s=0;s<t.length;s++)t[s]={id:e[s],doc:t[s]?JSON.parse(t[s]):null};return t})},a.has=function(e){return ej(e=this.db.transaction("reg","readonly").objectStore("reg").getKey(e)).then(function(e){return!!e})},a.search=null,a.info=function(){},a.transaction=function(e,t,s){e+="reg"!==e&&this.field?":"+this.field:"";let o=this.h[e+":"+t];if(o)return s.call(this,o);let n=this.db.transaction(e,t);this.h[e+":"+t]=o=n.objectStore(e);let i=s.call(this,o);return this.h[e+":"+t]=null,ej(n).finally(function(){return i})},a.commit=async function(e){let t=e.commit_task,s=[];e.commit_task=[];for(let e=0,o;e<t.length;e++)(o=t[e]).del&&s.push(o.del);s.length&&await this.remove(s),e.reg.size&&(await this.transaction("map","readwrite",function(t){for(let s of e.map){let e=s[0],o=s[1];o.length&&(t.get(e).onsuccess=function(){var s;let n=this.result;if(n&&n.length){let e=Math.max(n.length,o.length);for(let t=0,i,a;t<e;t++)if((a=o[t])&&a.length){if((i=n[t])&&i.length)for(s=0;s<a.length;s++)i.push(a[s]);else n[t]=a;s=1}}else n=o,s=1;s&&t.put(n,e)})}}),await this.transaction("ctx","readwrite",function(t){for(let s of e.ctx){let e=s[0];for(let o of s[1]){let s=o[0],n=o[1];n.length&&(t.get(e+":"+s).onsuccess=function(){var o;let i=this.result;if(i&&i.length){let e=Math.max(i.length,n.length);for(let t=0,s,a;t<e;t++)if((a=n[t])&&a.length){if((s=i[t])&&s.length)for(o=0;o<a.length;o++)s.push(a[o]);else i[t]=a;o=1}}else i=n,o=1;o&&t.put(i,e+":"+s)})}}}),e.store?await this.transaction("reg","readwrite",function(t){for(let s of e.store){let e=s[0],o=s[1];t.put("object"==typeof o?JSON.stringify(o):1,e)}}):e.bypass||await this.transaction("reg","readwrite",function(t){for(let s of e.reg.keys())t.put(1,s)}),e.tag&&await this.transaction("tag","readwrite",function(t){for(let s of e.tag){let e=s[0],o=s[1];o.length&&(t.get(e).onsuccess=function(){let s=this.result;s=s&&s.length?s.concat(o):o,t.put(s,e)})}}),e.map.clear(),e.ctx.clear(),e.tag&&e.tag.clear(),e.store&&e.store.clear(),e.document||e.reg.clear())},a.remove=function(e){return"object"!=typeof e&&(e=[e]),Promise.all([this.transaction("map","readwrite",function(t){t.openCursor().onsuccess=function(){let t=this.result;t&&eD(t,e)}}),this.transaction("ctx","readwrite",function(t){t.openCursor().onsuccess=function(){let t=this.result;t&&eD(t,e)}}),this.transaction("tag","readwrite",function(t){t.openCursor().onsuccess=function(){let t=this.result;t&&eD(t,e,!0)}}),this.transaction("reg","readwrite",function(t){for(let s=0;s<e.length;s++)t.delete(e[s])})])},e.s(["default",0,{Index:eE,Charset:D,Encoder:C,Document:eh,Worker:Y,Resolver:ei,IndexedDB:eN,Language:{}}],761947);var eF=e.i(395774);let eH=e=>"string"==typeof e?e:Array.isArray(e)?e.map(eH).join(" "):e&&"object"==typeof e&&e.children&&Array.isArray(e.children)?e.children.map(eH).join(" "):"";
2e.s(["processDocument",0,(e,t,s)=>{let o,n,i,a=(e=>{let t=e.match(/^---\s*\n([\s\S]*?)\n---/);if(t){let e=t[1].match(/"title":\s*"([^"]*)"/);if(e)return e[1]}return""})(s),r=(e=>{try{let t=eF.default.parse(e),s=eF.default.transform(t);return eH(s)}catch(e){return console.error("Error parsing Markdoc:",e),""}})(s),l=(o=[],n=eF.default.parse(s),(i=e=>{if("heading"===e.type&&e.children){let t=e.children.map(e=>"string"==typeof e?e:e.text||"").join("").trim();t&&o.push(t)}e.children&&e.children.forEach(i)})(n),o);return{id:e,title:a,url:t,content:r.replace(/\s+/g," ").trim(),headings:l}}],917910)},961504,e=>{"use strict";var t=e.i(318008),s=e.i(35203),o=e.i(983888),n=e.i(244142),i=e.i(967508),a=e.i(944967),r=e.i(398145),l=e.i(687652),c=e.i(122047),u=e.i(552099);let d={md:{hideBelowDesktop:"md:hidden",showAtDesktop:"md:flex",flexColAtDesktop:"md:flex-col",stickyAtDesktop:"md:sticky",selfStartAtDesktop:"md:self-start",fixedAtDesktop:"md:fixed"},lg:{hideBelowDesktop:"lg:hidden",showAtDesktop:"lg:flex",flexColAtDesktop:"lg:flex-col",stickyAtDesktop:"lg:sticky",selfStartAtDesktop:"lg:self-start",fixedAtDesktop:"lg:fixed"},xl:{hideBelowDesktop:"xl:hidden",showAtDesktop:"xl:flex",flexColAtDesktop:"xl:flex-col",stickyAtDesktop:"xl:sticky",selfStartAtDesktop:"xl:self-start",fixedAtDesktop:"xl:fixed"}};e.s(["Sidebar",0,e=>{let h,p,m,f,g,y,w,b,v,k,S,T,_,I,R,C,x,A,M,E,U,L,P,O,N,D,j,F,H,$,q,B,G,W,z,V,K,Y,J=(0,s.c)(96),{menuContent:X,menuBottomContent:Q,desktopWidthClassName:Z,desktopBreakpoint:ee,desktopNavPaddingClassName:et,desktopLogoPaddingClassName:es,logoSrc:eo,logoAlt:en,logoWidth:ei,logoHeight:ea,logoAlign:er,logoHref:el,hideLogo:ec,desktopPositionClassName:eu,desktopPosition:ed,variant:eh,persistedScrollTop:ep,scrollRestoreKey:em,onScrollPositionChange:ef}=e,eg=void 0===Z?"md:w-64":Z,ey=void 0===et?"px-2":et,ew=void 0===es?"px-4":es,eb=void 0===eo?"/meticulous_logo.svg":eo,ev=void 0===en?"Meticulous Logo":en,e
2k=void 0===ei?32:ei,eS=void 0===ea?32:ea,eT=void 0===er?"center":er,e_=void 0!==ec&&ec,eI=void 0===eu?"md:inset-y-0":eu,eR="light"===(void 0===eh?"dark":eh),eC="sticky"===(void 0===ed?"fixed":ed),ex=d[void 0===ee?"md":ee];J[0]!==ev||J[1]!==eS||J[2]!==eb||J[3]!==ek?(h=(0,t.jsx)(c.Image,{src:eb,alt:ev,width:ek,height:eS}),J[0]=ev,J[1]=eS,J[2]=eb,J[3]=ek,J[4]=h):h=J[4];let eA=h,{isOpen:eM,onClose:eE}=(0,u.useSidebarContext)(),eU=(0,l.useRef)(null),eL=(0,l.useRef)(null);J[5]!==eM||J[6]!==ep?(p=()=>{void 0!==ep&&(eU.current&&(eU.current.scrollTop=ep),eM&&eL.current&&(eL.current.scrollTop=ep))},J[5]=eM,J[6]=ep,J[7]=p):p=J[7],J[8]!==eM||J[9]!==ep||J[10]!==em?(m=[ep,em,eM],J[8]=eM,J[9]=ep,J[10]=em,J[11]=m):m=J[11],(0,l.useLayoutEffect)(p,m),J[12]!==ef?(f=e=>{ef?.(e.currentTarget.scrollTop)},J[12]=ef,J[13]=f):f=J[13];let eP=f;J[14]!==ex.hideBelowDesktop?(g=(0,a.default)("fixed","inset-0","flex","z-40",ex.hideBelowDesktop),J[14]=ex.hideBelowDesktop,J[15]=g):g=J[15],J[16]!==eE?(y=()=>{eE?.()},J[16]=eE,J[17]=y):y=J[17],J[18]===Symbol.for("react.memo_cache_sentinel")?(w=(0,a.default)("transition-opacity","ease-linear","duration-300"),J[18]=w):w=J[18],J[19]===Symbol.for("react.memo_cache_sentinel")?(b=(0,a.default)("transition-opacity","ease-linear","duration-300"),J[19]=b):b=J[19],J[20]===Symbol.for("react.memo_cache_sentinel")?(v=(0,t.jsx)(n.Transition.Child,{as:l.Fragment,enter:w,enterFrom:"opacity-0",enterTo:"opacity-100",leave:b,leaveFrom:"opacity-100",leaveTo:"opacity-0",children:(0,t.jsx)(o.DialogBackdrop,{className:(0,a.default)("fixed","inset-0","bg-zinc-600/75")})}),J[20]=v):v=J[20],J[21]===Symbol.for("react.memo_cache_sentinel")?(k=(0,a.default)("transition","ease-in-out","duration-300","transform"),J[21]=k):k=J[21],J[22]===Symbol.for("react.memo_cache_sentinel")?(S=(0,a.default)("transition","ease-in-out","duration-300","transform"),J[22]=S):S=J[22];let eO=eR?"bg-white dark:bg-zinc-900":"bg-zinc-800";J[23]!==eO?(T=(0,a.default)("relative","flex-1","flex","flex-col","max-w-xs","w-full",eO),J[23]=eO,J[24]=T):T=J[24],J[25]===Symbol.for("react.memo_cache_sentinel")?(_=(0,a.default)("ease-in-out","duration-300"),J[25]=_):_=J[25],J[26]===Symbol.for("react.memo_cache_sentinel")?(I=(0,a.default)("ease-in-out","duration-300"),J[26]=I):I=J[26],J[27]===Symbol.for("react.memo_cache_sentinel")?(R=(0,a.default)("absolute","top-0","right-0","-mr-12","pt-2"),J[27]=R):R=J[27],J[28]===Symbol.for("react.memo_cache_sentinel")?(C=(0,a.default)("ml-1","flex","items-center","justify-center","h-10","w-10","rounded-full","focus:outline-hidden","focus:ring-2","focus:ring-inset","focus:ring-white"),J[28]=C):C=J[28],J[29]!==eE?(x=()=>{eE?.()},J[29]=eE,J[30]=x):x=J[30],J[31]===Symbol.for("react.memo_cache_sentinel")?(A=(0,t.jsx)("span",{className:"sr-only",children:"Close sidebar"}),J[31]=A):A=J[31],J[32]===Symbol.for("react.memo_cache_sentinel")?(M=(0,t.jsx)(i.XMarkIcon,{className:(0,a.default)("h-6","w-6","text-white"),"aria-hidden":"true"}),J[32]=M):M=J[32],J[33]!==x?(E=(0,t.jsx)(n.Transition.Child,{as:l.Fragment,enter:_,enterFrom:"opacity-0",enterTo:"opacity-100",leave:I,leaveFrom:"opacity-100",leaveTo:"opacity-0",children:(0,t.jsx)("div",{className:R,children:(0,t.jsxs)("button",{type:"button",className:C,onClick:x,children:[A,M]})})}),J[33]=x,J[34]=E):E=J[34];let eN=eR?"bg-white dark:bg-zinc-900":"bg-zinc-900";J[35]!==eN?(U=(0,a.default)("flex-1","h-0","overflow-y-auto",eN),J[35]=eN,J[36]=U):U=J[36],J[37]===Symbol.for("react.memo_cache_sentinel")?(L=(0,a.default)("mt-5","px-2","pb-10","space-y-4"),J[37]=L):L=J[37],J[38]!==Q||J[39]!==X?(P=(0,t.jsxs)("nav",{className:L,children:[X,Q]}),J[38]=Q,J[39]=X,J[40]=P):P=J[40],J[41]!==eP||J[42]!==U||J[43]!==P?(O=(0,t.jsx)("div",{ref:eL,onScroll:eP,className:U,children:P}),J[41]=eP,J[42]=U,J[43]=P,J[44]=O):O=J[44],J[45]!==T||J[46]!==E||J[47]!==O?(N=(0,t.jsx)(n.Transition.Child,{as:l.Fragment,enter:k,enterFrom:"-translate-x-full",enterTo:"translate-x-0",leave:S,leaveFrom:"translate-x-0",leaveTo:"-translate-x-full",children:(0,t.jsxs)(o.DialogPanel,{className:T,children:[E,O]})}),J[45]=T,J[46]=E,J[47]=O,J[48]=N):N=J[48],J[49]===Symbol.for("react.memo_cache_sentinel")?(D=(0,t.jsx)("div",{className:(0,a.default)("shrink-0","w-14"),"aria-hidden":"true"}),J[49]=D):D=J[49],J[50]!==g||J[51]!==y||J[52]!==N?(j=(0,t.jsxs)(o.Dialog,{as:"div",className:g,onClose:y,children:[v,N,D]}),J[50]=g,J[51]=y,J[52]=N,J[53]=j):j=J[53],J[54]!==eM||J[55]!==j?(F=(0,t.jsx)(n.Transition.Root,{show:eM,as:l.Fragment,children:j}),J[54]=eM,J[55]=j,J[56]=F):F=J[56],J[57]!==ex.fixedAtDesktop||J[58]!==ex.flexColAtDesktop||J[59]!==ex.selfStartAtDesktop||J[60]!==ex.showAtDesktop||J[61]!==ex.stickyAtDesktop||J[62]!==eI||J[63]!==eg||J[64]!==eR||J[65]!==eC?(H=(0,a.default)("hidden",ex.showAtDesktop,eg,ex.flexColAtDesktop,eC?(0,a.default)(ex.stickyAtDesktop,ex.selfStartAtDesktop):ex.fixedAtDesktop,eI,"border-r",eR?"border-zinc-200 dark:border-zinc-800":"border-zinc-800"),J[57]=ex.fixedAtDesktop,J[58]=ex.flexColAtDesktop,J[59]=ex.selfStartAtDesktop,J[60]=ex.showAtDesktop,J[61
2]=ex.stickyAtDesktop,J[62]=eI,J[63]=eg,J[64]=eR,J[65]=eC,J[66]=H):H=J[66];let eD=eR?"bg-white dark:bg-zinc-900":"bg-black";J[67]!==eD?($=(0,a.default)("flex-1","flex","flex-col","min-h-0",eD),J[67]=eD,J[68]=$):$=J[68],J[69]===Symbol.for("react.memo_cache_sentinel")?(q=(0,a.default)("flex-1","flex","flex-col","overflow-y-auto"),J[69]=q):q=J[69],J[70]!==ew||J[71]!==e_||J[72]!==eT||J[73]!==el||J[74]!==eA?(B=e_?null:(0,t.jsx)("div",{className:(0,a.default)("flex","shrink-0","items-center","left"===eT?"justify-start":"justify-center","mt-6",ew),children:el?(0,t.jsx)(r.default,{href:el,children:eA}):eA}),J[70]=ew,J[71]=e_,J[72]=eT,J[73]=el,J[74]=eA,J[75]=B):B=J[75];let ej=e_?"mt-4":"mt-5";return J[76]!==ey||J[77]!==ej?(G=(0,a.default)(ej,"flex-1",ey,"space-y-4"),J[76]=ey,J[77]=ej,J[78]=G):G=J[78],J[79]!==Q||J[80]!==X||J[81]!==G?(W=(0,t.jsxs)("nav",{className:G,children:[X,Q]}),J[79]=Q,J[80]=X,J[81]=G,J[82]=W):W=J[82],J[83]!==eP||J[84]!==B||J[85]!==W?(z=(0,t.jsxs)("div",{ref:eU,onScroll:eP,className:q,children:[B,W]}),J[83]=eP,J[84]=B,J[85]=W,J[86]=z):z=J[86],J[87]!==$||J[88]!==z?(V=(0,t.jsx)("div",{className:$,children:z}),J[87]=$,J[88]=z,J[89]=V):V=J[89],J[90]!==H||J[91]!==V?(K=(0,t.jsx)("div",{className:H,children:V}),J[90]=H,J[91]=V,J[92]=K):K=J[92],J[93]!==F||J[94]!==K?(Y=(0,t.jsxs)(t.Fragment,{children:[F,K]}),J[93]=F,J[94]=K,J[95]=Y):Y=J[95],Y}])},88865,e=>{"use strict";let t;var s,o=e.i(39786);let n=`---
3{
4  "title": "Getting Started with Meticulous"
5}
6---
7
8# {% $frontmatter.title %}
9
10Meticulous records your interactions with your application on environments such as localhost as you develop it. It then monitors which lines of code and edge
11cases in your application are tested by each user flow, and from this curates a suite of tests that aims to exhaustively cover every edge
12case. As your application evolves, so does this test suite.
13
14By reducing the maintenance cost of a test to exactly zero, Meticulous is able to dramatically scale the number of tests, and from this
15provide a level of coverage that is unattainable with manually written tests.
16
17When you open a pull request Meticulous replays those selected user sessions against both the new and old version of the app, grouping and
18surfacing any differences to you to highlight the different edge cases your change triggers.
19
20To set Meticulous up you need to:
21
221. [Connect your GitHub, GitLab, or Bitbucket repository](${o.ONBOARDING_GUIDE_URL}#1-connect-your-repository)
232. Choose either [automated CLI onboarding](${o.ONBOARDING_GUIDE_URL}#automated-setup-with-meticulous-onboard) or the manual path:
24   - [Install the session recorder](${o.INSTALL_RECORDER_URL})
25   - [Set up tests to run in CI](${o.CI_SETUP_URL})
263. Record a session and verify Meticulous reports a result on a pull request.
27
28Continue to the [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) to get started. For additional
29questions about how Meticulous works, check out the [FAQ and Troubleshooting](${o.FAQ_AND_TROUBLESHOOTING_URL}) section.
30`;var i=e.i(474338),a=e.i(854443),r=e.i(838643);let l=`
31{% tabs tabNameSpace="provider" %}
32{% tab label="GitHub" %}
33
341. Sign in to [Meticulous](https://app.meticulous.ai) and create or select your organization.
352. Choose **Connect to GitHub** when creating the project.
363. [Install the Meticulous GitHub App](${i.METICULOUS_GITHUB_APP_INSTALL_URL}) for the organization and repositories you want Meticulous to test.
374. Return to Meticulous, select the repository, and create the linked project.
38
39{% /tab %}
40{% tab label="GitLab" %}
41
42${r.linkGitLabInstructions}
43
44{% /tab %}
45{% tab label="Bitbucket" %}
46
47${a.linkBitbucketInstructions}
48
49{% /tab %}
50{% /tabs %}
51`,c=`---
52{
53  "title": "Meticulous Onboarding Guide"
54}
55---
56
57# {% $frontmatter.title %}
58
59Set up Meticulous in this order:
60
611. Connect the repository to Meticulous.
622. Choose automated CLI onboarding or manual setup.
633. Record sessions and confirm Meticulous runs on pull requests.
64
65Connecting the repository first lets Meticulous identify the base and head
66versions of each pull request or merge request and publish its test result in
67the right place.
68
69---
70
71## 1. Connect your repository
72
73Create your Meticulous organization and project, then connect the Git provider
74that hosts the repository. Do this before installing the recorder or configuring
75CI.
76
77${l}
78
79Once the linked project exists, choose how you want to install Meticulous.
80
81---
82
83## 2. Choose your setup path
84
85{% tabs tabNameSpace="setup-path" %}
86{% tab label="Meticulous CLI (recommended)" %}
87
88## Automated setup with meticulous onboard
89
90\`meticulous onboard\` uses Claude Code or Codex on your machine to inspect the
91application and prepare a pull request containing the recorder and CI
92configuration. Meticulous does not host the model inference; the command uses
93your existing Claude Code or Codex account.
94
95### Prerequisites
96
97- Run the command from a clone of the connected Git repository.
98- Install and authenticate [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex](https://developers.openai.com/codex/cli/).
99- Use Node.js 20 or newer.
100
101### Run onboarding
102
103From the application repository. If you are not logged in, the command opens a
104browser to sign in, then continues:
105
106{% command_card %}
107\`\`\`bash
108npx @alwaysmeticulous/cli onboard --project="{% project_slug /%}"
109\`\`\`
110{% /command_card %}
111
112On a remote machine where a browser cannot reach this terminal, sign in first
113with device login, then re-run onboard:
114
115{% command_card hideProjectSelector=true %}
116\`\`\`bash
117npx @alwaysmeticulous/cli auth login --device
118\`\`\`
119{% /command_card %}
120
121It asks you to choose the frontend application in a monorepo and the local
122coding agent, reviews the repository, proposes a plan for approval, and then
123opens a setup pull request.
124
125After the pull request is ready:
126
1271. Review and merge the recorder and CI changes.
1282. Add any requested API token to your CI provider&apos;s secret store.
1293. Record a representative session.
1304. Open a pull request and confirm that Meticulous reports a result.
131
132{% /tab %}
133{% tab label="Manual setup" %}
134
135## Manual recorder and CI setup
136
137Use the guided setup in the Meticulous app or follow these docs:
138
1391. [Install the recorder](${o.INSTALL_RECORDER_URL}) for localhost and your
140   trusted internal or preview environments.
1412. Exercise a representative user flow and confirm the session appears in the
142   Meticulous project.
1433. [Replay the session locally](${o.DETECT_DIFFS_LOCALLY_URL}) before moving to
144   CI. Debugging locally is faster than debugging a CI-only failure.
1454. [Choose a CI approach](${o.CI_SETUP_URL}):
146   [upload static assets or a container](${o.GITHUB_ACTIONS_SETUP_URL}).
147
148For authenticated applications, make sure the recorded flow can sign in and
149replay reliably. See [Troubleshooting authentication](${o.TROUBLESHOOT_AUTH_URL})
150and [recording and replaying across environments](${o.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}).
151
152{% /tab %}
153{% /tabs %}
154
155---
156
157## 3. Verify the complete setup
158
159Setup is complete when:
160
161- The project is linked to the correct GitHub, GitLab, or Bitbucket repository.
162- At least one representative session reaches Meticulous.
163- A session replays successfully against your application.
164- The default branch has a baseline test run.
165- A pull request or merge request produces a Meticulous result.
166
167If something fails, start with [recorder troubleshooting](${o.TROUBLESHOOT_RECORDER_URL})
168or the [FAQ and troubleshooting guide](${o.FAQ_AND_TROUBLESHOOTING_URL}).
169
170After the first successful run, [make the Meticulous check blocking](${o.MAKE_CHECK_BLOCKING_URL})
171and [reduce false-positive diffs](${o.FIX_FALSE_POSITIVES_URL}).
172`,u=`
173There are two ways to add the Meticulous recorder to your web application:
174 1. [By inserting it as script tag](${o.INSTALL_RECORDER_AS_SCRIPT_TAG_INSTALLATION_INSTRUCTIONS_URL}) **(recommended)**
175 2. [By installing an NPM package](${o.INSTALL_RECORDER_AS_NPM_DEPENDENCY_INSTALLATION_INSTRUCTIONS_URL})
176
177If possible, we recommend that you use the **script tag** as it is the only way to fully guarantee that the recorder
178initializes before any other scripts execute, thereby ensuring Meticulous can capture all network responses
179([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})).
180
181For bundler-based setups such as Vite, rsbuild, and Nuxt, our script tag instructions use the \`@alwaysmeticulous/recorder-plugin\`
182dev dependency to inject the script automatically at build time.
183
184However, if it's not possible to template your HTML so that the script tag is only included in the environments where you
185want to record sessions then you can use [the loader package instead](${o.INSTALL_RECORDER_AS_NPM_DEPENDENCY_INSTALLATION_INSTRUCTIONS_URL}).
186`,d=`---
187{
188  "title": "Get started with Meticulous Recorder"
189}
190---
191
192# {% $frontmatter.title %}
193
194Meticulous recorder is a tool for recording real user sessions. The recorder captures your users' actions and any network requests
195(and responses) during their session. Please note that although plaintext passwords are redacted, the recorded network requests can
196include authorization tokens and other headers -- you should therefore only add trusted users to your Meticulous organization.
197You can either add the recorder to all environments or just internal non-production environments.
198
199## 1. Create and connect your project
200
201Sign up at [https://app.meticulous.ai/signup](https://app.meticulous.ai/signup). You will be prompted to create an organization and
202project. Connect the project to its GitHub, GitLab, or Bitbucket repository before installing the recorder.
203
204## 2. Install the Meticulous recorder
205
206You can run \`npx @alwaysmeticulous/cli onboard --project="<ORGANIZATION>/<PROJECT>"\` from the connected repository to have
207Claude Code or Codex prepare the recorder and CI changes, or install the recorder manually:
208
209${u}
210`,h=`---
211{
212  "title": "Setting up Meticulous to test your pull requests"
213}
214---
215
216# {% $frontmatter.title %}
217
218There are two ways to run Meticulous tests on your pull requests. We recommend the following approaches, in order of preference:
219
2201. **Upload static assets** — If your app can be served as a folder of static files (HTML/JS/CSS), this is the simplest approach. Not suitable for apps that require server-side rendering (e.g. Next.js). [Get started here](${o.GITHUB_ACTIONS_SETUP_URL}).
2212. **Upload a container image** — If your app requires a server (e.g. Next.js, SSR), upload a Docker image and we'll run it for you. This is the recommended approach for most apps. [Get started here](${o.GITHUB_ACTIONS_SETUP_URL}).
222`;var p=e.i(932576);let m=`
223# Important: The workflow needs to run both on pushes to your main branch and on
224# pull requests. It needs to run on your main branch because it'll use the results
225# from the base commit of the PR on the main branch to compare against.
226on:
227  push:
228    branches:
229      - main
230  pull_request: {}
231  # Important: We need the workflow to be triggered on workflow_dispatch events,
232  # so that Meticulous can run the workflow on the base commit to compare
233  # against if an existing workflow hasn't run. The meticulous-commit-sha input
234  # lets Meticulous ask for a specific commit (e.g. stacked PRs); without it,
235  # a dispatched run can only build whatever the branch currently points at.
236  workflow_dispatch:
237    inputs:
238      meticulous-commit-sha:
239        description: Commit Meticulous has asked this run to build. Defaults to the branch head.
240        required: false`,f=(e="${{ secrets.METICULOUS_API_TOKEN }}")=>`      # Same workflow file as the upload step — ensure-base dispatches *this*
241      # workflow on the base branch. Run it before checkout/build so the base
242      # can start while this job continues. Needs no checkout. Pass the same
243      # ref as checkout so we pre-warm the base the upload step will ask for.
244      - name: Ensure base tests exist
245        uses: ${i.GITHUB_ACTION_ENSURE_BASE_NAME}@v1
246        with:
247          api-token: ${e}
248          ref: \${{ env.METICULOUS_COMMIT_SHA }}
249
250      - name: Checkout repository
251        uses: actions/checkout@v4
252        with:
253          ref: \${{ env.METICULOUS_COMMIT_SHA }}`,g=`name: Meticulous
254${m}
255
256# Important: The workflow needs all the permissions below.
257# These permissions are mainly needed to post and update the status check and
258# feedback comment on your PR. Meticulous won't work without them.
259permissions:
260  actions: write
261  contents: read
262  issues: write
263  pull-requests: write
264  statuses: read
265
266env:
267  # Prefer the dispatched commit when set; otherwise the PR head. On pull_request github.sha is the merge commit,
268  # not the PR head SHA that Meticulous looks up.
269  METICULOUS_COMMIT_SHA: \${{ github.event.inputs['meticulous-commit-sha'] || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
270
271jobs:
272  test:
273    name: Meticulous
274    runs-on: ubuntu-latest
275
276    steps:
277${f()}`,y=`
278{% callout_card variant="warning" title="Important: Static Asset URLs" %}
279Meticulous automatically swaps the base URL (origin) for navigation and API requests, but **static assets (CSS, JS, images) referenced with absolute URLs in your HTML are NOT automatically rewritten**.
280
281If your HTML contains absolute URLs like:
282\`\`\`html
283<script src="https://example.com/dist/app.js"></script>
284<link href="https://example.com/styles/main.css" rel="stylesheet">
285\`\`\`
286
287You should change them to relative URLs:
288\`\`\`html
289<script src="/dist/app.js"></script>
290<link href="/styles/main.css" rel="stylesheet">
291\`\`\`
292
293This ensures assets are loaded from the correct test environment rather than the original recording environment.
294{% /callout_card %}
295`,w=`
296{% callout_card variant="warning" title="Important: Files your server generates at runtime" %}
297We serve your uploaded directory exactly as you uploaded it — none of your own server code is in the loop. **Any file your real server writes at container start, or generates per-request, will not exist.**
298
299The most common example is a runtime environment-config script that your entry HTML loads, which your container's entrypoint writes from environment variables:
300
301\`\`\`html
302<!-- index.html -->
303<script src="/_env.js"></script>
304\`\`\`
305
306In an uploaded build there is nothing to write that file, so the request 404s. If your app reads its config as it boots, it will throw before it renders and **every test will simulate against a blank page**.
307
308To fix it, write a static version of the file into the directory you upload. Gate it on \`METICULOUS_BUILD\` so it only applies to builds for Meticulous:
309
310\`\`\`yaml
311      - name: Build project
312        env:
313          METICULOUS_BUILD: "true"
314        run: |
315          pnpm build
316          # Emit the runtime config that production generates at container
317          # start, so the uploaded build can boot on its own.
318          ./scripts/write-env-js.sh > dist/_env.js
319\`\`\`
320
321The values only need to be good enough for your app to boot — Meticulous serves your recorded network responses rather than calling your real backend.
322
323If a static build can't be made self-sufficient, use the **Upload container image** workflow instead: that runs your real entrypoint, so anything it generates at startup is present as usual.
324{% /callout_card %}
325`,b=`
326${g}
327
328      - name: Install pnpm
329        uses: pnpm/action-setup@v4
330        with:
331          version: 10
332          run_install: false
333
334      - name: Use Node.js LTS
335        uses: actions/setup-node@v4
336        with:
337          node-version: "24"
338          cache: pnpm
339
340      - name: Cache node_modules
341        uses: actions/cache@v4
342        with:
343          path: node_modules
344          key: node-modules-\${{ runner.os }}-\${{ hashFiles('**/pnpm-lock.yaml') }}
345          restore-keys: |
346            node-modules-\${{ runner.os }}
347
348      - name: Install dependencies
349        run: |
350          pnpm install --frozen-lockfile
351
352      - name: Build project
353        # METICULOUS_BUILD marks this as a build for Meticulous testing.
354        env:
355          METICULOUS_BUILD: "true"
356        run: |
357          pnpm build
358`,v=`---
359{
360  "title": "Setting up Meticulous tests to run in your CI provider"
361}
362---
363
364# {% $frontmatter.title %}
365
366In this guide, we'll show you how to set up Meticulous to run in your CI system.
367
368{% tabs tabNameSpace="provider" %}
369{% tab label="GitHub" %}
370
371## 1. Install the Meticulous GitHub App
372
373If you haven't already connected this repository in [Connect your repository](${o.ONBOARDING_GUIDE_URL}#1-connect-your-repository), visit [${i.METICULOUS_GITHUB_APP_INSTALL_URL}](${i.METICULOUS_GITHUB_APP_INSTALL_URL}) to install our GitHub App.
374
375## 2. Add your Meticulous API token as a secret to your GitHub repository
376
377Select the project below that contains the sessions you wish to simulate, copy
378and paste the API token, and add it to your GitHub repository as a secret named
379\`METICULOUS_API_TOKEN\`:
380
381{% code_with_project_selector %}
382METICULOUS_API_TOKEN:
383{% standalone_api_token /%}
384{% /code_with_project_selector %}
385
386*Be very careful with this API token, since it allows the holder access to your recorded sessions.*
387
388{% expand title="How do I add it as a secret to my GitHub repository?" %}
389Open your repo and go to the settings tab:
390
391![Settings tab](https://assets.meticulous.ai/docs/repo-settings-tab.png)
392
393Select the actions tab within the secrets tab:
394
395![Secrets tab](https://assets.meticulous.ai/docs/actions-secrets-tab.png)
396
397And click the new repository secret button:
398
399![New repository secret button](https://assets.meticulous.ai/docs/new-repo-secret-button.png)
400
401Name the secret \`METICULOUS_API_TOKEN\`, and paste in the API token you copied from the previous step, and click add secret:
402
403![Add secret](https://assets.meticulous.ai/docs/new-secret-screen.png)
404{% /expand %}
405
406## 3. Add a GitHub Actions workflow to run your tests
407
408To run Meticulous on CI add a new \`.github/workflows/meticulous.yaml\` file, or, if you already use GitHub Actions, you
409can add it as a job to an existing workflow. The workflow needs to run on both [pushes to your main branch and on pull requests](${o.BRANCHES_REQUIRED_TO_RUN_ON_URL}).
410
411Put \`ensure-base\` as the first step of the same \`test\` job that uploads — same workflow file, before checkout. Pass the same \`ref\` as checkout (\`METICULOUS_COMMIT_SHA\` in this example). It works out the commit the upload step will compare against and, if that commit has no test run yet, dispatches this workflow and returns immediately so the base can build in parallel. The upload step still waits only if the base is missing when it finishes. Omit \`ref\` only if checkout uses \`github.sha\`.
412
413We offer two approaches to running Meticulous tests on CI. We recommend choosing the first approach that works for your app:
414
4151. **Upload your built assets** for us to test. This is the recommended approach if your app is a static site, i.e. it can be served as a folder of static assets (HTML/JS/CSS) without any server-side rendering or complex request rewriting. This approach is **NOT recommended** for Next.js applications as they typically cannot be served as static assets.
4162. **Upload a built container image** (e.g. a Docker image) for us to test. This is the recommended approach for most other apps, including Next.js applications. Almost any app can be containerized, so this is the universal fallback.
417
418{% tabs tabNameSpace="type" %}
419{% tab label="Upload static assets" %}
420
421This workflow file should use our \`upload-assets\` action to upload your built assets for us to test.
422
423See below for an example workflow file, which you can add to your repo. Note that you'll need to update it with the build steps for your app.
424
425File name: \`.github/workflows/meticulous.yaml\`.
426
427File contents:
428
429\`\`\`yaml
430# Workflow for building frontend and running Meticulous tests against static assets
431${b}
432      - name: Run Meticulous tests
433        uses: ${i.GITHUB_ACTION_UPLOAD_ASSETS_NAME}@v1
434        with:
435          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
436          # TODO: Update the directory path below to match your app's build output directory
437          # For example, if you're using Vite, this is typically "dist"
438          app-directory: "dist"
439\`\`\`
440
441${y}
442${w}
443{% /tab %}
444{% tab label="Upload container image" %}
445
446This workflow file should use our \`upload-container\` action to upload your built container image for us to test.
447
448Some requirements for the docker image you build are:
449- It should be built for the \`linux/amd64\` platform
450- It should respect the \`PORT\` environment variable, or if it doesn't, you should specify the port using the \`container-port\` input to the \`upload-container\` action.
451- It should respond to the \`GET /\` endpoint for a health check probe.
452
453You can provide additional environment variables, if needed, to the container using the \`container-env\` input to the \`upload-container\` action,
454specifying them as a newline-delimited list of \`NAME=value\` pairs.
455
456See below for an example workflow file, which you can add to your repo. Note that you'll need to update it with the build steps for your app.
457
458File name: \`.github/workflows/meticulous.yaml\`.
459
460File contents:
461
462\`\`\`yaml
463# Workflow for building frontend and running Meticulous tests against a container image
464${b}
465      - name: Set up Docker Buildx
466        uses: docker/setup-buildx-action@v3
467
468      - name: Docker Build (no push)
469        uses: docker/build-push-action@v6
470        with:
471          context: .
472          tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
473          push: false
474          # Marks the image as a Meticulous build. Consume it in your
475          # Dockerfile with \`ARG METICULOUS_BUILD\` / \`ENV METICULOUS_BUILD=$METICULOUS_BUILD\`
476          # if you need it at build time (e.g. getStaticProps / static generation).
477          build-args: |
478            METICULOUS_BUILD=true
479
480      - name: Run Meticulous tests
481        uses: ${i.GITHUB_ACTION_UPLOAD_CONTAINER_NAME}@v1
482        with:
483          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
484          image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
485          # Optional inputs:
486          container-port: 1234
487          # METICULOUS_BUILD is also passed at runtime so server-side code (e.g.
488          # getServerSideProps) can detect the Meticulous replay.
489          container-env: |
490            MY_ENV_VAR=my-value
491            METICULOUS_BUILD=true
492\`\`\`
493
494${y}
495{% /tab %}
496{% /tabs %}
497
498If you hit any issues then email [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll help you get set up.
499
500{% expand title="Naming workflows, jobs and secrets in a monorepo (recommended)" %}
501
502If your repository only ever ships one frontend, the generic names from the example
503above (\`.github/workflows/meticulous.yaml\`, workflow \`name: Meticulous\`,
504\`METICULOUS_API_TOKEN\` secret) are fine and you can skip this section.
505
506If your repository is a monorepo with more than one frontend, **or might host another
507Meticulous-tested frontend later**, per-app naming from the start makes future expansion
508painless: a second project can be added side-by-side without renaming the existing
509workflow file, job, or repository secret. The convention costs nothing on day one and
510keeps later additions contained to a new file.
511
512Two pieces of identity drive everything:
513
514- **\`<app-kebab>\`** — lowercase hyphenated, usually the last path segment of the
515  app you're onboarding (e.g. an app at \`apps/dashboard\` becomes \`dashboard\`). Used
516  in the workflow filename, the workflow \`name:\`, and the job \`name:\`.
517- **\`<APP_SLUG>\`** — the same identity as \`SCREAMING_SNAKE_CASE\` (e.g. \`dashboard\`
518  becomes \`DASHBOARD\`, \`marketing-site\` becomes \`MARKETING_SITE\`). Used in the GitHub
519  repository secret name and every \`secrets.*\` expression that reads it. A second
520  Meticulous project on the same monorepo later picks a different \`<APP_SLUG>\`, so
521  the two never collide.
522
523The convention we recommend:
524
525| | Recommended | Avoid |
526| --- | --- | --- |
527| New workflow file | \`.github/workflows/meticulous-<app-kebab>.yml\` | \`.github/workflows/meticulous.yaml\` |
528| Workflow YAML top-level \`name:\` | \`Meticulous (<app-kebab>)\` | bare \`Meticulous\` |
529| Job \`name:\` (\`jobs.<id>.name\`) | \`Meticulous (<app-kebab>)\` | bare \`Meticulous\` |
530| GitHub repository secret | \`METICULOUS_API_TOKEN_<APP_SLUG>\` | bare \`METICULOUS_API_TOKEN\` |
531| YAML reference to the API token | \`\${{ secrets.METICULOUS_API_TOKEN_<APP_SLUG> }}\` | \`\${{ secrets.METICULOUS_API_TOKEN }}\` |
532
533We also recommend scoping the workflow to the selected app's directory (and the
534shared UI libraries it imports) using \`paths:\` filters on both \`push\` and
535\`pull_request\` triggers, so the workflow only runs on commits that actually touch
536the relevant code.
537
538Pulling those together for an app at \`apps/dashboard\` (so \`<app-kebab>\` is
539\`dashboard\` and \`<APP_SLUG>\` is \`DASHBOARD\`):
540
541\`\`\`yaml
542# .github/workflows/meticulous-dashboard.yml
543name: Meticulous (dashboard)
544
545on:
546  push:
547    branches: [main]
548    paths:
549      - "apps/dashboard/**"
550      # any UI libraries the app imports:
551      - "packages/ui/**"
552  pull_request:
553    paths:
554      - "apps/dashboard/**"
555      - "packages/ui/**"
556  workflow_dispatch:
557    inputs:
558      meticulous-commit-sha:
559        description: Commit Meticulous has asked this run to build. Defaults to the branch head.
560        required: false
561
562permissions:
563  actions: write
564  contents: read
565  issues: write
566  pull-requests: write
567  statuses: read
568
569env:
570  # Prefer the dispatched commit when set; otherwise the PR head. On pull_request github.sha is the merge commit,
571  # not the PR head SHA that Meticulous looks up.
572  METICULOUS_COMMIT_SHA: \${{ github.event.inputs['meticulous-commit-sha'] || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
573
574jobs:
575  test:
576    name: Meticulous (dashboard)
577    runs-on: ubuntu-latest
578
579    steps:
580${f("${{ secrets.METICULOUS_API_TOKEN_DASHBOARD }}")}
581      - uses: actions/setup-node@v4
582        with:
583          node-version: "24"
584          cache: pnpm
585      - run: pnpm install --frozen-lockfile
586      - run: pnpm --filter dashboard build
587        env:
588          METICULOUS_BUILD: "true"
589      - uses: ${i.GITHUB_ACTION_UPLOAD_ASSETS_NAME}@v1
590        with:
591          api-token: \${{ secrets.METICULOUS_API_TOKEN_DASHBOARD }}
592          app-directory: "apps/dashboard/dist"
593\`\`\`
594
595When a second app on the same monorepo is later onboarded to Meticulous, copy this
596file to \`meticulous-<other-app-kebab>.yml\` and substitute the second app's
597\`<app-kebab>\` and \`<APP_SLUG>\` — the existing workflow stays untouched.
598
599If a CLI step in the workflow reads \`$METICULOUS_API_TOKEN\` directly (for example a
600script that calls \`npx @alwaysmeticulous/cli\` outside the action), re-expose the
601suffixed secret under the bare environment-variable name on that job or step:
602
603\`\`\`yaml
604jobs:
605  test:
606    # ...
607    env:
608      METICULOUS_API_TOKEN: \${{ secrets.METICULOUS_API_TOKEN_<APP_SLUG> }}
609\`\`\`
610
611The GitHub repository secret name and every \`\${{ secrets.* }}\` expression still use
612the suffixed form; only the in-job environment variable is re-exposed under the
613generic name.
614
615{% /expand %}
616
617{% expand title="Choosing the runner size (optional)" %}
618
619The example workflow uses \`runs-on: ubuntu-latest\` — GitHub's free runner. Meticulous's
620build + replay step can be resource-heavy, so a larger runner can roughly halve the
621wall-clock time of the job at extra cost. GitHub provides progressively larger labels
622such as \`ubuntu-latest-4-cores\`, \`ubuntu-latest-8-cores\`, and \`ubuntu-latest-16-cores\`
623(the exact labels available depend on your account's plan and any
624[larger runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners)
625you have configured).
626
627If you already build the app on a larger runner in another workflow, the simplest
628choice is to use the same \`runs-on\` label here so the Meticulous job has at least as
629much capacity as your normal build. Otherwise \`ubuntu-latest\` is a safe starting
630point — you can scale up later if the job runs slowly.
631
632{% /expand %}
633
634{% expand title="Enable source maps (recommended)" %}
635
636Meticulous uses source maps to attribute coverage to the original files in your repository
637so you can see which parts of your code are exercised by the tested sessions. The cleanest
638way to enable them is **inside this Meticulous workflow only**, via a CLI flag or
639environment variable on the build command — your committed build config stays untouched,
640and your other workflows (PR builds, production deploys) keep their existing behaviour.
641
642Pick the snippet for your framework and apply it to the \`Build project\` step of the
643example workflow above:
644
645**Vite** — pass \`--sourcemap\` to \`vite build\`:
646
647\`\`\`yaml
648      - name: Build project
649        run: pnpm build -- --sourcemap
650\`\`\`
651
652\`--sourcemap\` covers JavaScript only — Vite emits no CSS source maps for production builds at all. If you also want
653coverage attributed to your stylesheets, add \`@alwaysmeticulous/recorder-plugin/css-sourcemap\` to your Vite config, as
654described in the
655[Viewing source coverage information in Meticulous guide](${o.ENABLE_SOURCE_COVERAGE_URL}).
656
657**Create React App** — set \`GENERATE_SOURCEMAP=true\`:
658
659\`\`\`yaml
660      - name: Build project
661        env:
662          GENERATE_SOURCEMAP: "true"
663        run: pnpm build
664\`\`\`
665
666**Angular CLI** — pass \`--source-map\` to \`ng build\`:
667
668\`\`\`yaml
669      - name: Build project
670        run: pnpm exec ng build --source-map
671\`\`\`
672
673**webpack (custom config)** — set \`SOURCEMAP=true\` in CI and read it from
674\`webpack.config.js\`:
675
676\`\`\`yaml
677      - name: Build project
678        env:
679          SOURCEMAP: "true"
680        run: pnpm build
681\`\`\`
682
683\`\`\`js
684// webpack.config.js
685module.exports = (env, argv) => ({
686  // ...
687  devtool: process.env.SOURCEMAP === "true" ? "source-map" : argv.devtool,
688});
689\`\`\`
690
691**Next.js** and **Vue CLI** don't accept a build-time flag for this; they require a
692one-line config change:
693
694- Next.js — add \`productionBrowserSourceMaps: true\` to \`next.config.js\` (covers App
695  Router and Pages Router).
696- Vue CLI — add \`productionSourceMap: true\` to \`vue.config.js\`.
697
698These settings are safe to leave on permanently; they don't change runtime behaviour.
699
700Source maps must be served alongside the built assets — either as \`.map\` files in the
701same directory, via \`sourceMappingURL\` comments in the bundles, or via the \`SourceMap\`
702HTTP header. The \`upload-assets\` and \`upload-container\` actions pick them up
703automatically when they sit next to the bundles in your build output.
704
705For monorepo source maps that span multiple packages, see the
706[Viewing source coverage information in Meticulous guide](${o.ENABLE_SOURCE_COVERAGE_URL}).
707
708{% callout_card variant="warning" title="Cloud Replay only" %}
709If you use cloud replay against a public preview URL (Vercel, Netlify, etc.), enabling
710source maps will expose them on that public URL. If you would like coverage in this case,
711email [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) — we can help you scope
712source-map publishing to the default branch or switch to \`upload-assets\` /
713\`upload-container\` where they stay internal.
714{% /callout_card %}
715
716{% /expand %}
717
718### GitHub Action Configuration Reference
719
720All available inputs are documented in the action definition files:
721- [\`ensure-base\`](https://github.com/alwaysmeticulous/report-diffs-action/blob/main/ensure-base/action.yml) - First step before upload: dispatch a missing base build so it runs in parallel with the PR build. Pass the same \`ref\` as checkout.
722- [\`upload-assets\`](https://github.com/alwaysmeticulous/report-diffs-action/blob/main/upload-assets/action.yaml) - Upload static assets for testing (recommended for static sites)
723- [\`upload-container\`](https://github.com/alwaysmeticulous/report-diffs-action/blob/main/upload-container/action.yml) - Upload a container image for testing
724- [\`report-diffs-action\`](https://github.com/alwaysmeticulous/report-diffs-action/blob/main/action.yml) - Run tests in GitHub Actions runner (legacy)
725
726## 4. Validate that your workflow is working correctly
727
728Create a new pull request to add the above workflow. Then validate that Meticulous is able to access your application
729correctly and is successfully simulating sessions by viewing the test run for your PR in the Meticulous UI.
730
731{% callout_card variant="info" title="PR comments are off by default" %}
732Comments on PRs are disabled by default for new projects (this is an admin-only setting). You'll be able to see all test runs in the Meticulous UI under your project's "Test runs" tab. If you'd like to enable PR comments for your project, contact us at [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
733{% /callout_card %}
734
735## 5. Merge the PR to add your new GitHub workflow, and open a new PR to test Meticulous
736
737Merge the PR to add the above workflow. You won&apos;t see any results on the PR that adds the workflow because you need to wait for the workflow to run on your main branch for it to detect any diffs.
738
739Once the PR has merged and Meticulous has run on your base branch you can open a new PR to test Meticulous. The test run will be visible in the Meticulous UI under your project's "Test runs" tab, where you can review any visual diffs before merging your PR.
740
741If PR comments are enabled for your project, Meticulous will also post a comment on the PR if it changed any of the screens or logic for the workflows you've recorded sessions for:
742
743![Meticulous comment](https://assets.meticulous.ai/docs/github-actions-v2-006.png)
744
745## 6. (Optional) Require approving diffs before merging a PR
746
747If you've installed the [Meticulous GitHub App](https://github.com/apps/alwaysmeticulous) Meticulous will add a check on your PR that is red
748if there are diffs that haven't been approved yet and becomes green once you click the green 'Approve all Visual Differences' button.
749This button can be found on the test run page in the Meticulous UI (or by clicking the link in the Meticulous PR comment, if comments are enabled).
750
751If you wish, you can make this check blocking by following the instructions [here](${o.MAKE_CHECK_BLOCKING_URL}). Doing so will prevent developers
752from merging a PR which has visual differences until they have clicked the button to acknowledge the differences.
753
754{% /tab %}
755{% tab label="GitLab" %}
756
757If you are able to build your app such that it can be served as a folder of static assets (HTML/JS/CSS) without any server-side rendering or complex request rewriting,
758then you can use our \`ci upload-assets\` CLI command to upload your built assets for us to test.
759
760## 1. Link GitLab to Meticulous
761
762If you haven't already connected this repository in [Connect your repository](${o.ONBOARDING_GUIDE_URL}#1-connect-your-repository), complete the steps below.
763
764${r.linkGitLabInstructions}
765
766## 2. Add your Meticulous API token as a CI/CD variable
767
768Select the project below that contains the sessions you wish to 
768simulate, copy and paste the API token, and add it to your GitLab project
769as a CI/CD variable named \`METICULOUS_API_TOKEN\`:
770
771{% code_with_project_selector %}
772METICULOUS_API_TOKEN:
773{% standalone_api_token /%}
774{% /code_with_project_selector %}
775
776*Be very careful with this API token, since it allows the holder access to your recorded sessions.*
777
778## 3. Add a GitLab CI/CD pipeline to run your tests
779
780To run Meticulous on CI, add a new \`.gitlab-ci.yml\` file to your repository. The pipeline needs to run on both pushes to your main branch and on merge requests.
781
782This pipeline should use our \`ci upload-assets\` CLI command to upload your built assets for us to test.
783
784File name: \`.gitlab-ci.yml\`
785
786File contents:
787
788\`\`\`yaml
789stages:
790  - build
791  - test
792
793variables:
794  NODE_VERSION: "24"
795
796build:
797  stage: build
798  image: node:24-alpine
799  # METICULOUS_BUILD marks this as a build for Meticulous testing.
800  variables:
801    METICULOUS_BUILD: "true"
802  script:
803    - pnpm install --frozen-lockfile
804    - pnpm build
805  artifacts:
806    paths:
807      - dist/
808    expire_in: 1 hour
809  only:
810    - main
811    - merge_requests
812
813test:
814  stage: test
815  image: node:24-alpine
816  dependencies:
817    - build
818  script:
819    - >
820      npx @alwaysmeticulous/cli ci upload-assets
821      --apiToken="$METICULOUS_API_TOKEN"
822      --appDirectory="dist"
823      --commitSha="$CI_COMMIT_SHA"
824      --waitForBase
825  only:
826    - main
827    - merge_requests
828\`\`\`
829
830**Important:** Make sure to update the \`appDirectory\` path to match your app's build output directory. For example, if you're using Vite, this is typically "dist".
831
832{% expand title="Naming jobs and variables in a monorepo (recommended)" %}
833
834If your repository only ever ships one frontend, the generic names from the example
835above (\`meticulous:\` job, \`METICULOUS_API_TOKEN\` variable) are fine and you can skip
836this section.
837
838If your repository is a monorepo with more than one frontend, **or might host another
839Meticulous-tested frontend later**, per-app naming from the start makes future expansion
840painless: a second project can be added side-by-side without renaming the existing job
841or CI/CD variable. The convention costs nothing on day one and keeps later additions
842contained to a new job (or a new included pipeline file).
843
844Two pieces of identity drive everything:
845
846- **\`<app-kebab>\`** — lowercase hyphenated, usually the last path segment of the
847  app you're onboarding (e.g. an app at \`apps/dashboard\` becomes \`dashboard\`). Used
848  in the job key and the optional included file name.
849- **\`<APP_SLUG>\`** — the same identity as \`SCREAMING_SNAKE_CASE\` (e.g. \`dashboard\`
850  becomes \`DASHBOARD\`, \`marketing-site\` becomes \`MARKETING_SITE\`). Used in the GitLab
851  CI/CD variable name and every YAML reference to it. A second Meticulous project on
852  the same monorepo later picks a different \`<APP_SLUG>\`, so the two never collide.
853
854The convention we recommend:
855
856| | Recommended | Avoid |
857| --- | --- | --- |
858| Job key in \`.gitlab-ci.yml\` (or included pipeline file) | \`meticulous-<app-kebab>:\` | bare \`meticulous:\` |
859| GitLab CI/CD variable | \`METICULOUS_API_TOKEN_<APP_SLUG>\` | bare \`METICULOUS_API_TOKEN\` |
860| YAML reference to the API token | \`$METICULOUS_API_TOKEN_<APP_SLUG>\` | bare \`$METICULOUS_API_TOKEN\` |
861| Optional included pipeline file | \`.gitlab/ci/meticulous-<app-kebab>.yml\` (then \`include:\` it from \`.gitlab-ci.yml\`) | a second bare \`meticulous\` block in \`.gitlab-ci.yml\` |
862
863We also recommend scoping the job to the selected app's path (and the shared UI
864libraries it imports) using \`rules: changes:\`, so the Meticulous job only runs on
865commits that actually touch the relevant code. If your existing pipeline uses
866\`only:\` instead of \`rules:\`, mirror that style with \`only: changes:\`.
867
868Pulling those together for an app at \`apps/dashboard\` (so \`<app-kebab>\` is
869\`dashboard\` and \`<APP_SLUG>\` is \`DASHBOARD\`):
870
871\`\`\`yaml
872meticulous-dashboard:
873  stage: test
874  image: node:24-alpine
875  variables:
876    METICULOUS_BUILD: "true"
877  rules:
878    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
879      changes:
880        - "apps/dashboard/**/*"
881        # any UI libraries the app imports:
882        - "packages/ui/**/*"
883    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
884      changes:
885        - "apps/dashboard/**/*"
886        - "packages/ui/**/*"
887  script:
888    - pnpm install --frozen-lockfile
889    - pnpm --filter dashboard build
890    - >
891      npx @alwaysmeticulous/cli ci upload-assets
892      --apiToken="$METICULOUS_API_TOKEN_DASHBOARD"
893      --appDirectory="apps/dashboard/dist"
894      --commitSha="$CI_COMMIT_SHA"
895      --waitForBase
896\`\`\`
897
898When a second app on the same monorepo is later onboarded to Meticulous, copy this
899block and substitute the second app's \`<app-kebab>\` and \`<APP_SLUG>\` — the existing
900job stays untouched.
901
902If you'd rather expose the suffixed variable under the bare \`METICULOUS_API_TOKEN\`
903name inside the job (for example because the build script reads
904\`process.env.METICULOUS_API_TOKEN\` directly), add a job-scoped \`variables:\` mapping:
905
906\`\`\`yaml
907meticulous-<app-kebab>:
908  # ...
909  variables:
910    METICULOUS_API_TOKEN: $METICULOUS_API_TOKEN_<APP_SLUG>
911\`\`\`
912
913The CI/CD variable name and every direct YAML reference still use the suffixed form;
914only the in-job environment variable is re-exposed under the generic name.
915
916{% /expand %}
917
918{% expand title="Choosing the image and tags (optional)" %}
919
920\`image:\` controls the Docker image used for the job (Node version, OS). The example
921above uses \`node:24-alpine\`; if your existing pipeline uses a different Node version,
922or a non-Alpine image (e.g. \`node:24\` for native build tooling that needs glibc),
923use the same image for the Meticulous job. If your pipeline references a project-level
924\`NODE_VERSION\` variable (e.g. \`image: node:\${NODE_VERSION}-alpine\`), reuse that variable
925rather than hard-coding the version.
926
927\`tags:\` controls which registered runner picks up the job, and you usually do not need
928to set it. Most projects rely on a default runner configured at the project or group
929level, and adding tags can route the job to a runner that doesn't exist. If your
930existing pipeline already sets \`tags:\` on build-heavy jobs (literal strings — not
931\`$VAR\` or \`!reference\` indirection), copy the same list onto the Meticulous job.
932
933If you're on GitLab.com SaaS shared runners and the default \`saas-linux-small-amd64\`
934turns out to be too slow for Meticulous's build + replay, you can opt into a larger
935runner by adding \`tags: [saas-linux-large-amd64]\` (or similar). This is optional and
936only applies to GitLab.com SaaS — self-managed instances configure runner sizes
937differently.
938
939{% /expand %}
940
941{% expand title="Enable source maps (recommended)" %}
942
943Meticulous uses source maps to attribute coverage to the original files in your repository
944so you can see which parts of your code are exercised by the tested sessions. The cleanest
945way to enable them is **inside this Meticulous pipeline only**, via a CLI flag or
946environment variable on the build command — your committed build config stays untouched,
947and your other pipelines (MR builds, production deploys) keep their existing behaviour.
948
949Pick the snippet for your framework and apply it to the \`build\` job of the example
950pipeline above:
951
952**Vite** — pass \`--sourcemap\` to \`vite build\`:
953
954\`\`\`yaml
955build:
956  script:
957    - pnpm install --frozen-lockfile
958    - pnpm build -- --sourcemap
959\`\`\`
960
961\`--sourcemap\` covers JavaScript only — Vite emits no CSS source maps for production builds at all. If you also want
962coverage attributed to your stylesheets, add \`@alwaysmeticulous/recorder-plugin/css-sourcemap\` to your Vite config, as
963described in the
964[Viewing source coverage information in Meticulous guide](${o.ENABLE_SOURCE_COVERAGE_URL}).
965
966**Create React App** — set \`GENERATE_SOURCEMAP=true\`:
967
968\`\`\`yaml
969build:
970  variables:
971    GENERATE_SOURCEMAP: "true"
972  script:
973    - pnpm install --frozen-lockfile
974    - pnpm build
975\`\`\`
976
977**Angular CLI** — pass \`--source-map\` to \`ng build\`:
978
979\`\`\`yaml
980build:
981  script:
982    - pnpm install --frozen-lockfile
983    - pnpm exec ng build --source-map
984\`\`\`
985
986**webpack (custom config)** — set \`SOURCEMAP=true\` in CI and read it from
987\`webpack.config.js\`:
988
989\`\`\`yaml
990build:
991  variables:
992    SOURCEMAP: "true"
993  script:
994    - pnpm install --frozen-lockfile
995    - pnpm build
996\`\`\`
997
998\`\`\`js
999// webpack.config.js
1000module.exports = (env, argv) => ({
1001  // ...
1002  devtool: process.env.SOURCEMAP === "true" ? "source-map" : argv.devtool,
1003});
1004\`\`\`
1005
1006**Next.js** and **Vue CLI** don't accept a build-time flag for this; they require a
1007one-line config change:
1008
1009- Next.js — add \`productionBrowserSourceMaps: true\` to \`next.config.js\` (covers App
1010  Router and Pages Router).
1011- Vue CLI — add \`productionSourceMap: true\` to \`vue.config.js\`.
1012
1013These settings are safe to leave on permanently; they don't change runtime behaviour.
1014
1015Source maps must be served alongside the built assets — either as \`.map\` files in the
1016same directory, via \`sourceMappingURL\` comments in the bundles, or via the \`SourceMap\`
1017HTTP header. The \`ci upload-assets\` and \`ci upload-container\` commands pick them up
1018automatically when they sit next to the bundles in your build output.
1019
1020For monorepo source maps that span multiple packages, see the
1021[Viewing source coverage information in Meticulous guide](${o.ENABLE_SOURCE_COVERAGE_
1021URL}).
1022
1023{% /expand %}
1024
1025## 4. Merge the MR to add your new GitLab CI/CD pipeline, and open a new MR to test Meticulous
1026
1027Merge the MR to add the above pipeline configuration. You won't see any results on the MR that adds the pipeline because you need to wait for the pipeline to run on your main branch for it to detect any diffs.
1028
1029Once the MR has merged and Meticulous has run on your base branch you can open a new MR to test Meticulous.
1030Comments are typically disabled when you first create a project in Meticulous, but you'll be able to see the test results within the Meticulous UI.
1031
1032{% /tab %}
1033{% tab label="BitBucket" %}
1034
1035If you are able to build your app such that it can be served as a folder of static assets (HTML/JS/CSS) without any server-side rendering or complex request rewriting,
1036then you can use our \`ci upload-assets\` CLI command to upload your built assets for us to test.
1037
1038## 1. Link Bitbucket to Meticulous
1039
1040If you haven't already connected this repository in [Connect your repository](${o.ONBOARDING_GUIDE_URL}#1-connect-your-repository), complete the steps below.
1041
1042${a.linkBitbucketInstructions}
1043
1044## 2. Add your Meticulous API token as a repository variable
1045
1046Select the project below that contains the sessions you wish to simulate, copy and paste the API token, and add it to your Bitbucket repository
1047as a secured repository variable named \`METICULOUS_API_TOKEN\`:
1048
1049{% code_with_project_selector %}
1050METICULOUS_API_TOKEN:
1051{% standalone_api_token /%}
1052{% /code_with_project_selector %}
1053
1054*Be very careful with this API token, since it allows the holder access to your recorded sessions.*
1055
1056## 3. Add a Bitbucket Pipelines configuration to run your tests
1057
1058To run Meticulous on CI, add a \`bitbucket-pipelines.yml\` file to your repository. The pipeline needs to run on both pushes to your main branch and on pull requests.
1059
1060This pipeline should use our \`ci upload-assets\` CLI command to upload your built assets for us to test.
1061
1062On pull request builds, Bitbucket merges the destination branch into the source branch during **Build Setup** before your steps run. Meticulous does **not** support testing that ephemeral merge commit. **Checkout the PR source tip** before building so uploads use a commit Bitbucket exposes via the API and the backend can compare against the **merge-base** with the destination branch.
1063
1064Add this step at the start of your pull-request pipeline script:
1065
1066\`\`\`bash
1067git reset --hard "$BITBUCKET_COMMIT"
1068\`\`\`
1069
1070The Meticulous CLI uploads \`git rev-parse HEAD\` (the source tip after the reset above). You do **not** need to pass \`--commitSha\` or \`--baseSha\` manually on PR pipelines.
1071
1072File name: \`bitbucket-pipelines.yml\`
1073
1074File contents:
1075
1076\`\`\`yaml
1077image: node:24
1078
1079pipelines:
1080  branches:
1081    main:
1082      - step:
1083          name: Build and test
1084          caches:
1085            - node
1086          script:
1087            - npm ci
1088            # METICULOUS_BUILD marks this as a build for Meticulous testing.
1089            - METICULOUS_BUILD=true npm run build
1090            - >
1091              npx @alwaysmeticulous/cli ci upload-assets
1092              --apiToken="$METICULOUS_API_TOKEN"
1093              --appDirectory="dist"
1094              --waitForBase
1095  pull-requests:
1096    "**":
1097      - step:
1098          name: Build and test
1099          caches:
1100            - node
1101          script:
1102            - git reset --hard "$BITBUCKET_COMMIT"
1103            - npm ci
1104            # METICULOUS_BUILD marks this as a build for Meticulous testing.
1105            - METICULOUS_BUILD=true npm run build
1106            - >
1107              npx @alwaysmeticulous/cli ci upload-assets
1108              --apiToken="$METICULOUS_API_TOKEN"
1109              --appDirectory="dist"
1110              --waitForBase
1111\`\`\`
1112
1113**Important:** Make sure to update the \`appDirectory\` path to match your app's build output directory. For example, if you're using Vite, this is typically "dist".
1114
1115{% /tab %}
1116{% /tabs %}
1117`,k=`---
1118{
1119  "title": "Running tests against existing deployment URLs"
1120}
1121---
1122
1123# {% $frontmatter.title %}
1124
1125{% callout_card variant="info" title="Preferred: Upload static assets or a container image" %}
1126If possible, we recommend [running tests via your CI pipeline](${o.GITHUB_ACTIONS_SETUP_URL}) by uploading static assets or a container image. These approaches are simpler and more reliable. Use deployment URL testing only if those options are not possible for your app.
1127{% /callout_card %}
1128
1129{% tabs %}
1130{% tab label="GitHub" %}
1131
1132If you use Vercel, Netlify, Cloudflare Pages or a similar system to generate PR preview URLs you can use the Meticulous GitHub app to test your PRs for you:
1133
1134#### **Step 1: Install the Meticulous GitHub app**
1135
1136Begin by [installing the Meticulous GitHub app](${i.METICULOUS_GITHUB_APP_INSTALL_URL}).
1137
1138#### **Step 2: Integrate with your preview URL provider**
1139
1140Once the GitHub app is installed, select the system you use to generate PR preview links:
1141
1142{% tabs tabNameSpace="preview-provider" %}
1143{% tab label="Vercel" %}
1144
1145Install the [Meticulous Vercel integration](${i.METICULOUS_VERCEL_INTEGRATION_INSTALL_URL}) and link your Vercel project in Meticulous.
1146
1147If you have multiple Vercel projects for your GitHub repo, or multiple environments that you deploy the same branches/commits to, then you'll
1148need to let Meticulous know which environments it should run the tests against. You can do so by navigating to your project page and clicking on the *'Settings'* tab.
1149
1150{% /tab %}
1151{% tab label="Netlify" %}
1152
1153If you use Netlify you can configure a Netlify webhook so tests are triggered when new preview deploys are ready. Contact
1154[${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) for help setting this up.
1155
1156{% /tab %}
1157{% tab label="Cloudflare" %}
1158
1159If you use Cloudflare pages you can configure a Cloudflare webhook so tests are triggered when new preview deploys are ready. Contact
1160[${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) for help setting this up.
1161
1162{% /tab %}
1163{% tab label="Other/Home-Grown" %}
1164
1165If you use another preview URL system, or a home grown system you can generate
1166a GitHub deployment (environment) whenever a commit is pushed to a branch. This can then in turn be used to trigger a Meticulous test run
1167against the new deployment.
1168
1169You can view instructions for how to do this [here](${o.CREATE_DEPLOYMENTS_ON_GITHUB_URL}), however it can be
1170fragile to set up correctly, and requires your PR preview system to have immutable, long-lived preview URLs and use identical
1171build settings across PR branches and main branch commits (to avoid false screenshot diffs). For this reason we recommend
1172[triggering Meticulous from your CI pipeline instead](${o.GITHUB_ACTIONS_SETUP_URL}), if possible.
1173
1174{% /tab %}
1175{% /tabs %}
1176
1177#### **Step 3 (optional): Make the Meticulous check blocking**
1178
1179Whenever you open a new pull request Meticulous will now simulate a set of sessions against the preview URL before and after the PR, and post
1180a comment to the PR notifying of any changes spotted.
1181
1182If you wish, you can make this check blocking by following the instructions [here](${o.MAKE_CHECK_BLOCKING_URL}). Doing so will prevent developers
1183from merging a PR which has visual differences until they have clicked the button to acknowledge the differences.
1184
1185{% /tab %}
1186{% tab label="GitLab" %}
1187
1188## Initial setup
1189
1190If you use Vercel, Netlify or a similar system to generate PR preview URLs, you can use Meticulous to test your PRs.
1191To set this up:
1192
1193${r.linkGitLabInstructions}
1194
1195## Further steps
1196
1197{% tabs %}
1198{% tab label="Vercel" %}
1199
1200Please let us know that you are using Vercel preview URLs in the email you sent us when setting up GitLab.
1201After some setup on our side Meticulous will automatically run tests against Vercel preview URLs whenever a new deployment is ready.
1202
1203{% /tab %}
1204{% tab label="Other preview URL providers" %}
1205
1206Call the */test-runs/trigger* endpoint from your GitLab CI pipeline whenever a new commit is pushed to a branch with an open MR.
1207The endpoint will trigger a test run, and Meticulous will handle setting commit statuses and posting notes to the merge request as
1208the test run progresses.
1209
1210{% code_with_project_selector %}
1211\`\`\`http
1212POST https://app.meticulous.ai/api/test-runs/trigger
1213
1214Headers: {
1215  authorization: "{% api_token /%}"
1216  Content-Type: "application/json"
1217}
1218
1219Body: {
1220  headSha: string, // the SHA of the commit you want to test
1221  headDeploymentUrl: string, // preview URL of headSha
1222  baseSha: string, // the SHA of the commit which the new test run will be compared against
1223  baseDeploymentUrl: string // preview URL of baseSha
1224}
1225\`\`\`
1226{% /code_with_project_selector %}
1227
1228There are two different types of pipelines that GitLab can trigger when a new commit is pushed to a branch with an open MR: *merge request
1229pipelines* and *merged results pipelines* ([GitLab docs](https://docs.gitlab.com/ee/ci/pipelines/merged_results_pipelines.html)). Your
1230pipeline should call the */test-runs/trigger* endpoint with different values for \`headSha\` and \`baseSha\` depending on which type of
1231pipeline you use.
1232
1233If you use merge request pipelines:
1234- \`headSha\` should be the SHA of the commit that was just pushed to the branch. This is exposed in the CI pipeline as
1235\`$CI_COMMIT_SHA\`.
1236- \`baseSha\` should be the SHA of the commit from which the branch was created. This is exposed in the CI pipeline as
1237\`$CI_MERGE_REQUEST_DIFF_BASE_SHA\`.
1238
1239If you use merged results pipelines:
1240- \`headSha\` should be the SHA of the merge commit. This is exposed in the CI pipeline as \`$CI_COMMIT_SHA\`.
1241- \`baseSha\` should be the SHA of the HEAD commit on the target branch. This is exposed in the CI pipeline as
1242\`$CI_MERGE_REQUEST_TARGET_BRANCH_SHA\`.
1243
1244{% /tab %}
1245{% /tabs %}
1246
1247{% /tab %}
1248{% /tabs %}
1249`,S={anchorTagId:"base-urls",title:"How does Meticulous compute the URL to simulate a session against?",body:`
1250When Meticulous simulates sessions it is configured to simulate the sessions against a particular base URL, which will likely be different to the URL the
1251session was recorded at.
1252
1253For example, if Meticulous is set up with GitHub Actions, then the base URL will be the URL you pass as the \`appUrl\` to \`report-diffs-action\`, for example \`http://localhost:3000\`.
1254
1255If Meticulous is set up to use preview URLs, from Vercel or similar services, then the base URL will be the preview URL of the deployment, for example \`https://tps-reports-app-37tz-initech.vercel.app\`. If there are multiple deployments
1256Meticulous will look for one to an environment that is included under \`Environments to Test Against\` in your Meticulous project settings.
1257
1258When simulating a session, Meticulous takes the URL the session was recorded at and swaps out the origin with the new base URL. So if the session was recorded
1259at \`https://www.initech.com/some/path?query=paramValue\`, and you're running the Meticulous tests against \`https://tps-reports-app-37tz-initech.vercel.app\`, then Meticulous will simulate the session at \`https://tps-reports-app-37tz-initech.vercel.app/some/path?query=paramValue\`.
1260
1261**Important limitation**: This base URL swapping applies to:
1262- Page navigation URLs
1263- API requests (fetch/XHR)
1264
1265However, it does **NOT** apply to static assets (CSS, JavaScript, images) that are referenced with absolute URLs directly in your HTML. For example, if your HTML contains \`<script src="https://www.initech.c
1265om/app.js"></script>\`, this URL will not be rewritten. To ensure assets load correctly across environments, use relative URLs like \`<script src="/app.js"></script>\` instead. See [Troubleshooting Cross-Environment Issues](${o.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}) for more details.
1266
1267You'll therefore need to make sure that the base URL you are simulating sessions against (\`https://tps-reports-app-37tz-initech.vercel.app\`) serves up the same app under the same configuration as the base URL sessions are recorded on (\`https://www.initech.com\`).`},T={anchorTagId:"branches-must-run-on",title:"What branches does Meticulous need to run on, and against which environments?",body:`
1268Meticulous works by simulating sessions against the head commit of each pull request and comparing the results to the base commit of the pull request.
1269
1270It therefore needs to run on your main branch (e.g. main, master or develop) so that it has visual snapshots to compare against. And it also needs to run on any branches that you open pull requests from.
1271
1272If you're using Vercel, Netlify, or similar preview URLs, then Meticulous will compare snapshots from the preview URL of the base commit on the main branch to snapshots from the preview URL of the head commit of the pull request branch.
1273
1274In this case the environment variables and configuration you use to run & build your app needs to be the same for the deployments of the main branch (production deploys) and the deployments of pull request branches (preview deploys). If this isn't the case Meticulous could display false screenshot differences.
1275
1276For example if you configure production deploys of your app (from the main branch) to have a blue banner, and preview deploys of your app (from pull request branches) to have a red banner, then Meticulous would display screenshot diffs of the banner changing from blue to red for every screen. You want to make sure that the only screenshot diffs Meticulous shows are due to changes in the code introduced by the pull request being tested, rather than environmental differences between the environments tested on.
1277
1278You can learn how to avoid this [here](${o.FIX_FALSE_POSITIVES_URL}), and you can learn more about testing across environments [here](${o.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}).
1279
1280If, instead of preview URLs, you're using the \`report-diffs-action\` GitHub action, then Meticulous will compare snapshots from running your app from the base commit of the main branch to snapshots from running your app from the head commit of the pull request branch. In this case it's similarly important to make sure that you compile and run your app with the same configuration for both the main branch and the pull request branches.`},_={anchorTagId:"cli-in-ci-limitations",title:"Are there any limitations to using the Meticulous CLI to trigger tests in CI?",body:`
1281You might use the Meticulous CLI instead of the GitHub action if you don't use GitHub Actions for CI and don't want to use an additional CI provider. However, there are some limitations to consider when using the CLI in CI.
1282
1283The CLI offers two different approaches:
1284
1285#### **Upload Assets**
1286- *When to use:* If your app can be built into a single directory of static assets that can be served with a command like \`pnpm serve\`
1287- *Command:* \`ci upload-assets\`
1288- *Limitations relative to the GitHub action:* None
1289
1290#### **Upload Container**
1291- *When to use:* If your app can't be built into a single directory of static assets and needs a server to run (e.g. a Next.js app)
1292- *Command:* \`ci upload-container\`
1293- *Limitations relative to the GitHub action:* None
1294`},I={anchorTagId:"cross-environment-record-replay",title:"Can I record sessions from one environment (for example, production, or localhost) and simulate them against another environment (for example, a preview URL)?",body:`
1295  Yes. However the sessions may fail to simulate if there are significant differences between the environments. Please see the [Record and Simulate on Different Environments](${o.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}) page for more details.`},R={anchorTagId:"data-variants",title:"How does Meticulous handle network requests? / How does Meticulous ensure test coverage over different user types, data variants, or feature flag combinations?",body:`
1296  By default Meticulous will record the network responses (XHR, Fetch & Web Sockets) in the initial session when it is recorded.
1297  These responses will be stored alongside the session, and when the session is later replayed against another commit Meticulous will
1298  automatically stub out the requests with the appropriate responses. Matching within a session is sequence-aware, so mutation-then-GET
1299  flows keep the responses that originally followed those mutations. Similarly Meticulous will record and replay local storage, session storage
1300  and cookie values.
1301
1302  This means that if you have two sessions recorded under different users, and the network responses return different data for each user,
1303  then when the sessions are replayed each session will get the correct original data and you'll be able to test over both cases.
1304
1305  Meticulous's session selection algorithms will automatically select sessions to cover all the different user types, data variants, and feature flag
1306  combinations that lead to different behavior in your code/app. See the [Selecting Which Sessions to Run](${o.TESTING_POOL_URL}) page for details.
1307
1308  Automatically stubbing out the network responses allows Meticulous to ensure your tests are fast, fully deterministic and flake and
1309  side effect free. If you make a breaking API change and recorded responses get out of date, Meticulous first tries to **patch** the
1310  affected sessions using newer recordings of the same endpoint shape (preferring schema updates while keeping the original session's
1311  values where possible). Sessions that no longer add unique coverage are replaced by newer ones that cover the same lines of code /
1312  edge cases. For the full explanation - including why stubs do not need to be perfect for frontend blast-radius testing - see
1313  [Network Recording & Patching](${o.NETWORK_RECORDING_AND_PATCHING_URL}).
1314
1315  ### What is and isn't stubbed/mocked
1316
1317  **Stubbed by Meticulous:**
1318  - XHR (XMLHttpRequest) requests
1319  - Fetch API requests
1320  - WebSocket connections
1321  - Local storage, session storage, and cookies
1322
1323  **NOT stubbed by Meticulous:**
1324  - Static assets (CSS, JavaScript, images) loaded directly by the browser via HTML tags
1325  - Assets referenced with absolute URLs in your HTML (e.g., \`<script src="https://example.com/app.js">\`)
1326
1327  Static assets are loaded live from whatever URL they're referenced at. If you use absolute URLs for static assets in your HTML, those URLs will NOT be rewritten when testing against a different environment. We recommend using relative URLs (e.g., \`/dist/app.js\`) for static assets to ensure they load correctly across all test environments.
1328
1329  However if you wish to test your backend code with Meticulous you can do so by selecting which subset of requests to stub in the
1330  'Network Stubbing' tab in your Meticulous project's settings. If you're using NextJS with the app directory then Meticulous will
1331  automatically pass through requests for React server components if you select the 'Stub all requests, apart from requests for server
1332  components and static assets' option. This is the default behaviour for NextJS apps that use the app directory.
1333  `},C={anchorTagId:"dealing-with-localhost-sessions",title:"If Meticulous records sessions from half-finished branches on localhost won't that cause issues with the tests?",body:`
1334  The answer is no: Meticulous is designed to handle this case. It does so via two strategies:
1335
1336  1. Meticulous doesn't use every session recorded as a test but just [a subset that cover the maximal distinct edge cases and lines/branches
1337  of code](${o.TESTING_POOL_URL}). Broken sessions get filtered out by the session selection algorithms.
1338
1339  2. Meticulous takes the base screenshots for comparison at _replay_ time instead of at record time. When you open a PR we replay
1340  the selected sessions twice: once on the base commit and once on the head commit of the PR. We take screenshots and compare them. If it does replay a
1341  session from localhost that, for example, clicks on a feature that isn’t pushed up yet, then that 'broken' session will generate the same screenshots when
1342  replayed against both the base and the head commit. So it won’t create any false diffs.
1343  `},x={anchorTagId:"recorder-first-script",title:"Why does the Meticulous recorder script need to be the first script to execute?",body:`
1344See the [Ensure Recorder Captures All Requests](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL}) page for more details.`},A={anchorTagId:"recorder-performance-impact",title:"Does the Meticulous recorder impact my app's performance?",body:`
1345No. The Meticulous recorder is designed to have no meaningful impact on your application's performance. It operates passively by listening to browser events and recording network requests, without modifying your application's DOM or interfering with its execution.
1346
1347The recorder also monitors the size of data being recorded and will automatically abandon a session if the payload becomes too large, ensuring it never degrades the user's experience.`},M={anchorTagId:"session-selection",title:"How does Meticulous choose which sessions to run?",body:`
1348See the [Selecting Which Sessions to Run](${o.TESTING_POOL_URL}) page for details.`},E=[{section:"How Meticulous Works",questions:[{...R,title:"How does Meticulous handle network requests / BE calls?"},{...R,title:"How does Meticulous ensure test coverage over different user types, data variants, or feature flag combinations?"},M,C]},{section:"CI Setup",questions:[S,T,I,_]},{section:"Recorder Setup",questions:[A,x]}],U=(s=[...E.flatMap(({questions:e})=>e)],t=new Set,s.filter(e=>{let s=e.anchorTagId;return!t.has(s)&&(t.add(s),!0)})).map(e=>"data-variants"===e.anchorTagId?R:e),L=`---
1349{
1350  "title": "FAQ & Troubleshooting"
1351}
1352---
1353
1354# {% $frontmatter.title %}
1355
1356## Contents
1357
1358${E.map(({section:e,questions:t})=>"\n"+e+":\n"+t.map(({title:e,anchorTagId:t})=>`- [${e}](#${t})`).join("\n")).join("\n")}
1359
1360## Questions
1361
1362${U.map(({title:e,body:t,anchorTagId:s})=>'{% anchor id="'+s+'" /%}\n### '+e+"\n"+t).join("\n\n")}
1363
1364${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
1365`,P=`---
1366{
1367  "title": "Additional Guides"
1368}
1369---
1370
1371# {% $frontmatter.title %}
1372
1373 - [Getting started with backend testing](${o.ADDITIONAL_GUIDES.GETTING_STARTED_BACKEND_TESTING_URL})
1374 - [Install the backend recorder](${o.ADDITIONAL_GUIDES.INSTALL_BACKEND_RECORDER_URL})
1375 - [Exporting generated tests](${o.ADDITIONAL_GUIDES.EXPORTING_GENERATED_TESTS_URL})
1376 - [Not yet run checks](${o.ADDITIONAL_GUIDES.NOT_YET_RUN_CHECKS_URL})
1377`,O=`---
1378{
1379  "title": "Getting Started with Backend Testing"
1380}
1381---
1382
1383# {% $frontmatter.title %}
1384
1385Meticulous records your interactions with your application on environments such as localhost as you develop it, and replays those
1386sessions on every pull request to surface any differences your change triggers.
1387
1388If your app uses **server-side rendering (SSR)**, part of each user session happens on your backend: data is fetched on the server
1389before the page ever reaches the browser, so the frontend recorder alone never sees those requests. To test these apps, Meticulous
1390additionally records backend spans — the HTTP requests your server makes — and uses them to stub out server-side calls during
1391replay. This lets Meticulous accurately replay and diff server-rendered pages, catching regressions in your backend and SSR code
1392paths as well as your frontend.
1393
1394To set up backend testing you need to:
1395
13961. [Install the frontend session recorder](${o.ADDITIONAL_GUIDES.INSTALL_RECORDER_SCRIPT_FOR_BACKEND_TESTING_URL})
13972. [Install the backend spans recorder](${o.ADDITIONAL_GUIDES.INSTALL_BACKEND_RECORDER_URL})
13983. [Set up tests to run in CI](${o.GITHUB_ACTIONS_SETUP_URL})
13994. [Verify the complete setup](${o.ONBOARDING_GUIDE_URL}#3-verify-the-complete-setup)
1400
1401For additional questions about how Meticulous works, check out the [FAQ and Troubleshooting](${o.FAQ_AND_TROUBLESHOOTING_URL}) section.
1402`;var N=e.i(919275);let D=(e,t=!1)=>`
1403{% code_with_project_selector %}
1404{% tabs tabNameSpace="env" %}
1405{% tab label="Dev & Staging Only" %}
1406\`\`\`jsx
1407<${e}>
1408  ...
1409      {(process.env.NODE_ENV === "development" || process.env.VERCEL_ENV === "preview") && (
1410        // eslint-disable-next-line @next/next/no-sync-scripts
1411        <script
1412          data-recording-token="{% project_recording_token /%}"
1413          data-is-production-environment="false"${t?'\n          data-inject-session-id-header="true"':""}
1414          src="${N.SNIPPET_URL}"
1415        />
1416      )}
1417  ...
1418</${e}>
1419\`\`\`
1420{% /tab %}
1421{% tab label="All Environments" %}
1422\`\`\`jsx
1423<${e}>
1424  ...
1425      // eslint-disable-next-line @next/next/no-sync-scripts
1426      <script
1427      data-recording-token="{% project_recording_token /%}"
1428      data-is-production-environment={process.env.NODE_ENV === "production" || process.env.VERCEL_ENV === "production"}${t?'\n      data-inject-session-id-header="true"':""}
1429      src="${N.SNIPPET_URL}"
1430      />
1431  ...
1432</${e}>
1433\`\`\`
1434{% /tab %}
1435{% /tabs %}
1436{% /code_with_project_selector %}
1437`,j=(e=!1)=>`Install the Meticulous recorder plugin and add it to your Nuxt config. The plugin injects the recorder script as the first script tag in your app's \`<head>\`, with no async or defer attributes ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})).
1438
1439{% code %}
1440\`\`\`shell
1441npm install @alwaysmeticulous/recorder-plugin@latest --save-dev
1442\`\`\`
1443{% /code %}
1444
1445Modify your \`nuxt.config.ts\` file to include the plugin:
1446
1447{% code_with_project_selector %}
1448{% tabs tabNameSpace="env" %}
1449{% tab label="Dev Only (default)" %}
1450\`\`\`typescript
1451export default defineNuxtConfig({
1452  modules: [
1453    [
1454      "@alwaysmeticulous/recorder-plugin/nuxt",
1455      ${e?`{
1456        recordingToken: "{% project_recording_token /%}",
1457        attributes: { "data-inject-session-id-header": "true" },
1458      }`:'{ recordingToken: "{% project_recording_token /%}" }'},
1459    ],
1460  ],
1461});
1462\`\`\`
1463{% /tab %}
1464{% tab label="All Environments" %}
1465\`\`\`typescript
1466export default defineNuxtConfig({
1467  modules: [
1468    [
1469      "@alwaysmeticulous/recorder-plugin/nuxt",
1470      {
1471        recordingToken: "{% project_recording_token /%}",
1472        enabled: "always",${e?'\n        attributes: { "data-inject-session-id-header": "true" },':""}
1473      },
1474    ],
1475  ],
1476});
1477\`\`\`
1478{% /tab %}
1479{% /tabs %}
1480{% /code_with_project_selector %}
1481
1482By default, the plugin injects the recorder only during Nuxt development builds. If you set \`enabled: "always"\`, the plugin will inject the recorder in every environment and automatically set \`data-is-production-environment\` based on Nuxt's detected mode.
1483
1484By default Meticulous will stub out all requests to server side rendered pages, and so won't test server side rendered content. If you
1485use server side rendering and wish to test your server side rendered pages then please reach out to
1486[${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), or [book a call with us](${p.METICULOUS_SETUP_CALENDLY_LINK}), and we'll help you get set up.
1487`,F=(e=!1)=>`Install the Meticulous recorder plugin and add it to your rsbuild config. The plugin injects the recorder script as the first script tag in your app's \`<head>\`, with no async or defer attributes ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})).
1488
1489{% code %}
1490\`\`\`shell
1491npm install @alwaysmeticulous/recorder-plugin@latest --save-dev
1492\`\`\`
1493{% /code %}
1494
1495Modify your \`rsbuild.config.ts\` file to include the plugin:
1496
1497{% code_with_project_selector %}
1498{% tabs tabNameSpace="env" %}
1499{% tab label="Dev Only (default)" %}
1500\`\`\`typescript
1501import { defineConfig } from "@rsbuild/core";
1502import meticulous from "@alwaysmeticulous/recorder-plugin/rspack";
1503
1504export default defineConfig({
1505  tools: {
1506    rspack: {
1507      plugins: [
1508        meticulous({
1509          recordingToken: "{% project_recording_token /%}",${e?'\n          attributes: { "data-inject-session-id-header": "true" },':""}
1510        }),
1511      ],
1512    },
1513  },
1514});
1515\`\`\`
1516{% /tab %}
1517{% tab label="All Environments" %}
1518\`\`\`typescript
1519import { defineConfig } from "@rsbuild/core";
1520import meticulous from "@alwaysmeticulous/recorder-plugin/rspack";
1521
1522export default defineConfig({
1523  tools: {
1524    rspack: {
1525      plugins: [
1526        meticulous({
1527          recordingToken: "{% project_recording_token /%}",
1528          enabled: "always",${e?'\n          attributes: { "data-inject-session-id-header": "true" },':""}
1529        }),
1530      ],
1531    },
1532  },
1533});
1534\`\`\`
1535{% /tab %}
1536{% /tabs %}
1537{% /code_with_project_selector %}
1538
1539By default, the plugin injects the recorder only during non-production rsbuild builds. If you set \`enabled: "always"\`, the plugin will inject the recorder in every environment and automatically set \`data-is-production-environment\` based on Rspack's detected mode.
1540`,H=(e=!1)=>
1540`If you want to record sessions using Storybook, you can add the Meticulous recorder script tag by
1541creating a \`.storybook/preview-head.html\` file and adding the following:
1542
1543{% code_with_project_selector %}
1544\`\`\`html
1545<script
1546  data-recording-token="{% project_recording_token /%}"
1547  data-is-production-environment="false"${e?'\n  data-inject-session-id-header="true"':""}
1548  src="${N.SNIPPET_URL}"
1549></script>
1550<script>
1551  // Record and replay Storybook events sent from the parent (manager) to the
1552  // component iframe. These events capture interactions in Storybook controls
1553  // and actions (e.g., switching between stories).
1554  if (window.Meticulous?.replay) {
1555    window.Meticulous.replay.addCustomEventListener(
1556      "storybook-event",
1557      (serializedData) =>
1558        window.postMessage(serializedData, "*")
1559      ,
1560    )
1561  } else {
1562    window.addEventListener("message", event => {
1563      // Check if it's a storybook event
1564      try {
1565        const data = JSON.parse(event.data)
1566        if (data.key === "storybook-channel") {
1567          if (window.Meticulous?.record) {
1568            window.Meticulous.record.recordCustomEvent(
1569              "storybook-event",
1570              event.data,
1571            )
1572          }
1573        }
1574      } catch (e) {
1575        // Not a JSON message, ignore
1576      }
1577    })
1578  }
1579</script>
1580\`\`\`
1581{% /code_with_project_selector %}
1582
1583For TypeScript type definitions for the \`window.Meticulous\` object, see [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}).
1584`,$=(e=!1)=>`**(A)** Add the Meticulous recorder script tag in a \`<svelte:head>\` tag at the top of your \`__layout.svelte\` file. It's important the script is the
1585first script, and async and defer are not set to true ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})):
1586
1587{% code_with_project_selector %}
1588{% tabs tabNameSpace="env" %}
1589{% tab label="Dev & Staging Only" %}
1590\`\`\`svelte
1591<svelte:head>
1592{#if !import.meta.env.PROD}
1593  <script
1594    data-recording-token="{% project_recording_token /%}"
1595    data-is-production-environment="false"${e?'\n    data-inject-session-id-header="true"':""}
1596    src="${N.SNIPPET_URL}"
1597  ></script>
1598{/if}
1599</svelte:head>
1600\`\`\`
1601{% /tab %}
1602{% tab label="All Environments" %}
1603\`\`\`svelte
1604<svelte:head>
1605  <script
1606    data-recording-token="{% project_recording_token /%}"
1607    data-is-production-environment={import.meta.env.PROD}${e?'\n    data-inject-session-id-header="true"':""}
1608    src="${N.SNIPPET_URL}"
1609  ></script>
1610</svelte:head>
1611\`\`\`
1612{% /tab %}
1613{% /tabs %}
1614{% /code_with_project_selector %}
1615
1616**(B)** In your app.html, make sure that \`%svelte.head%\` is above any other scripts in the \`<head>\` tag:
1617
1618Good:
1619
1620{% code %}
1621\`\`\`html
1622<head>
1623    %svelte.head%
1624    <script src="another-script.js"></script>
1625</head>
1626\`\`\`
1627{% /code %}
1628
1629Bad:
1630
1631{% code %}
1632\`\`\`html
1633<head>
1634    <script src="another-script.js"></script>
1635    %svelte.head%
1636</head>
1637\`\`\`
1638{% /code %}
1639
1640**(C)** Wire through the MODE environment variable, and make sure MODE is set to \`production\` only for production builds:
1641
1642Add \`mode: process.env.MODE || 'development'\` to the \`vite\` section of your \`kit\` config in your \`svelte.config.js\` file. For example:
1643
1644{% code %}
1645\`\`\`javascript
1646const config = {
1647  kit: {
1648      vite: {
1649          // default to development as a guard
1650          mode: process.env.MODE || 'development',
1651      }
1652  },
1653}
1654\`\`\`
1655{% /code %}
1656
1657For all builds that get deployed to production, build your application using:
1658
1659\`\`\`bash
1660MODE=production npm run build
1661\`\`\`
1662
1663And for all other builds, including builds that get deployed to staging stacks and preview URLs, build your app using:
1664
1665\`\`\`bash
1666MODE=development npm run build
1667\`\`\`
1668
1669or
1670
1671\`\`\`bash
1672MODE=staging npm run build
1673\`\`\`
1674
1675
1676**(D)** If you want to test your server side rendered content, then contact us
1677
1678By default Meticulous will stub out all requests to server side rendered pages, and so won't test server side rendered content. If you
1679use server side rendering and wish to test your server side rendered pages then please reach out to
1680[${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), or [book a call with us](${p.METICULOUS_SETUP_CALENDLY_LINK}), and we'll help you get set up.
1681`,q=(e=!1)=>`Install the Meticulous recorder plugin and add it to your Vite config. The plugin injects the recorder script as the first script tag in your app's \`<head>\`, with no async or defer attributes ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})).
1682
1683{% code %}
1684\`\`\`shell
1685npm install @alwaysmeticulous/recorder-plugin@latest --save-dev
1686\`\`\`
1687{% /code %}
1688
1689Modify your \`vite.config.ts\` file to include the plugin:
1690
1691{% code_with_project_selector %}
1692{% tabs tabNameSpace="env" %}
1693{% tab label="Dev Only (default)" %}
1694\`\`\`typescript
1695import { defineConfig } from "vite";
1696import meticulous from "@alwaysmeticulous/recorder-plugin/vite";
1697
1698export default defineConfig({
1699  plugins: [
1700    meticulous({
1701      recordingToken: "{% project_recording_token /%}",${e?'\n      attributes: { "data-inject-session-id-header": "true" },':""}
1702    }),
1703  ],
1704});
1705\`\`\`
1706{% /tab %}
1707{% tab label="All Environments" %}
1708\`\`\`typescript
1709import { defineConfig } from "vite";
1710import meticulous from "@alwaysmeticulous/recorder-plugin/vite";
1711
1712export default defineConfig({
1713  plugins: [
1714    meticulous({
1715      recordingToken: "{% project_recording_token /%}",
1716      enabled: "always",${e?'\n      attributes: { "data-inject-session-id-header": "true" },':""}
1717    }),
1718  ],
1719});
1720\`\`\`
1721{% /tab %}
1722{% /tabs %}
1723{% /code_with_project_selector %}
1724
1725By default, the plugin injects the recorder only during Vite development builds. If you set \`enabled: "always"\`, the plugin will inject the recorder in every environment and automatically set \`data-is-production-environment\` based on Vite's detected mode.
1726`,B=`If it's not possible to meet these requirements then you can [use an NPM dependency instead of a script tag](${o.INSTALL_RECORDER_AS_NPM_DEPENDENCY_URL}). If you need to wait for a network request to complete before you know 
1726whether you should record the session then you can [buffer the requests in memory, and only send them later](${o.ADDITIONAL_GUIDES.CONTROLLING_WHEN_RECORDING_STARTS_AND_STOPS_URL}).`,G=({isNextJs:e,notPossibleToMeetRequirementsText:t})=>`
1727{% callout_card showIcon=false %}
1728${"yes"===e?"**Important: The Meticulous Recorder script should use the native `script` tag instead of the NextJS `Script` component, be the first script to load, and have no async or defer attributes**":"**Important: The Meticulous Recorder script should be the first script to load, and have no async or defer attributes**"}
1729
1730Libraries you depend on may snapshot references
1731to \`window.fetch\` or \`window.XMLHttpRequest\` early in the page lifecycle, which means if Meticulous is not the first script to load
1732it may not be able to record all the network
1733responses required for your app to function ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})). Therefore the recorder script
1734must be the first script to load in order to be guaranteed to capture all network requests correctly. This means:
1735
17361. It should be added to your \`index.html\` file, before any other script tags.
17372. It should not have any async or defer attributes set${"yes"===e?", and use the native `script` tag instead of the NextJS `Script` component":"."}${"maybe"===e?" If using NextJS then it should use the native `script` tag instead of the NextJS `Script` component.":""}
17383. It should be present in the initial HTML returned from the server -- you cannot add the script tag dynamically using JavaScript, since if
1739you do so the browser may execute the script after other scripts have loaded. If you need to include the script tag in your HTML only
1740in certain environments then this must be done either server-side, or at build time by templating your HTML.
1741
1742${t??B}
1743
1744{% /callout_card %}
1745`,W=`
1746## Validating installation
1747
1748Once you add the Meticulous snippet, open your webapp (either locally or on the environment that you injected the snippet into) and record a session by clicking around on your web app.
1749
1750If the snippet was installed successfully you should be able to view the recorded session in your
1751{% project_link %}Meticulous dashboard {% /project_link %} in the **Sessions** section.
1752
1753If you set a CSP policy on your application then you'll need to add [these](${o.RECORDER_CSP_EXCEPTIONS_URL}) CSP exceptions.
1754
1755## I've installed the snippet but why do I not see any sessions in my Meticulous dashboard?
1756
1757See [troubleshooting](${o.TROUBLESHOOT_RECORDER_URL}) for more information on why this might be happening.
1758
1759## Issues / questions?
1760
1761We're always happy to help you with any issues you encounter while setting up or anything you might be unsure about.
1762
1763Get in touch by emailing [[email protected]](mailto:[email protected]).
1764`,z=`If you have any issues setting up the recorder then click [here](${p.METICULOUS_SETUP_CALENDLY_LINK}) to book a call with us.`,V=`
1765If you have any cross-origin or sandboxed iFrames then the recorder should be added to each of these iFrames as well as the main frame. ${z}
1766
1767${W}
1768`,K=`---
1769{
1770  "title": "Install the Meticulous recorder via a script tag"
1771}
1772---
1773
1774{% anchor id="${o.INSTALLATION_INSTRUCTIONS_ANCHOR}" /%}
1775# {% $frontmatter.title %}
1776
1777This sets up the frontend recorder with the \`data-inject-session-id-header\` attribute enabled, so that requests made from the
1778browser carry an \`X-Meticulous-Session-Id\` header the [backend recorder](${o.ADDITIONAL_GUIDES.INSTALL_BACKEND_RECORDER_URL}) can use to
1779correlate them with this session.
1780
1781Please select your framework or build tool:
1782
1783{% tabs direction="grid" noTabSelectedByDefault=true %}
1784{% tab label="NextJS with the /pages directory" %}
1785## Installing on NextJS with the /pages directory
1786
1787${G({isNextJs:"yes"})}
1788
1789Add a script tag to your \`_document.js\` file within \`Head\`. If the layout doesn't yet have a \`<Head>\` tag then
1790you can add one within the \`<Html>\` tag.
1791
1792${D("Head",!0)}
1793
1794${V}
1795{% /tab %}
1796{% tab label="NextJS with the /app directory" %}
1797## Installing on NextJS with the /app directory
1798
1799${G({isNextJs:"yes"})}
1800
1801Add a script tag to your \`/app/layout.tsx\` or \`/app/layout.jsx\` file within \`head\`. If the layout doesn't yet have a \`<head>\` tag then
1802you can add one within the \`<html>\` tag.
1803
1804${D("head",!0)}
1805
1806After adding the snippet you'll need to follow a [few additional steps](${o.NEXTJS_APP_ROUTER_ADDITIONAL_SETUP_URL}) to ensure Meticulous can
1807correctly test your app.
1808
1809${V}
1810{% /tab %}
1811{% tab label="Nuxt" %}
1812## Installing on NuxtJS
1813
1814${G({isNextJs:"no"})}
1815
1816${j(!0)}
1817
1818${V}
1819{% /tab %}
1820{% tab label="SvelteKit" %}
1821## Installing on SvelteKit
1822
1823${G({isNextJs:"no"})}
1824
1825${$(!0)}
1826
1827${V}
1828{% /tab %}
1829{% tab label="Vite" %}
1830## Installing on Vite
1831
1832${G({isNextJs:"no"})}
1833
1834${q(!0)}
1835
1836${V}
1837{% /tab %}
1838{% tab label="rsbuild" %}
1839## Installing on rsbuild
1840
1841${G({isNextJs:"no"})}
1842
1843${F(!0)}
1844
1845${V}
1846{% /tab %}
1847{% tab label="Storybook" %}
1848## Installing on Storybook
1849
1850${G({isNextJs:"no"})}
1851
1852${H(!0)}
1853
1854${V}
1855{% /tab %}
1856{% tab label="Any other framework or build tool" %}
1857## Installing on any other framework or build tool
1858
1859${G({isNextJs:"no"})}
1860
1861Add the recorder as the first script tag in your \`<head>
1861\` tag. If you only want to record sessions in non-production environments then
1862you will need to template your HTML to only include the script tag in non-production environments (if this is not possible then you can
1863 [use an NPM dependency instead of a script tag](${o.INSTALL_RECORDER_AS_NPM_DEPENDENCY_INSTALLATION_INSTRUCTIONS_URL})).
1864
1865{% code_with_project_selector %}
1866\`\`\`html
1867<head>
1868  ...
1869  <script
1870    data-recording-token="{% project_recording_token /%}"
1871    data-is-production-environment="<true/false>"
1872    data-inject-session-id-header="true"
1873    src="${N.SNIPPET_URL}">
1874  </script>
1875
1876  <!--Meticulous snippet should be added before your app -->
1877  ...
1878  <script src="main_app.js"></script>
1879</head>
1880\`\`\`
1881{% /code_with_project_selector %}
1882
1883${V}
1884{% /tab %}
1885
1886{% /tabs %}
1887`,Y="@alwaysmeticulous/backend-recorder-launcher",J="@alwaysmeticulous/backend-recorder-workerd",X=`If you have any issues setting up the backend recorder then click [here](${p.METICULOUS_BACKEND_SETUP_CALENDLY_LINK}) to book a call with us.`,Q=`---
1888{
1889  "title": "Install the backend recorder"
1890}
1891---
1892
1893# {% $frontmatter.title %}
1894
1895The backend recorder captures the server side of your users' sessions. It intercepts HTTP requests and responses in your Node.js
1896app using OpenTelemetry approach and exports them to Meticulous, where they are used to stub out backend calls during replays. This means
1897Meticulous can replay sessions accurately even when they depend on data returned by your own API.
1898
1899The backend recorder is installed via the [\`${Y}\`](https://www.npmjs.com/package/${Y})
1900package from the Meticulous SDK. It is complementary to the [frontend recorder](${o.ADDITIONAL_GUIDES.INSTALL_RECORDER_SCRIPT_FOR_BACKEND_TESTING_URL})
1901— install the frontend recorder to capture user activity in the browser, and the backend recorder to capture the matching server-side requests.
1902
1903{% callout_card variant="info" title="When do I need the backend recorder?" %}
1904The backend recorder is only required when your app uses **server-side rendering (SSR)**. In SSR apps, data is fetched on the server
1905before the page reaches the browser, so the frontend recorder never sees those requests — the backend recorder captures them instead
1906so Meticulous can stub them during replay. If your app renders entirely on the client (e.g. a standard SPA), the frontend recorder
1907already captures every request and the backend recorder is not needed. ${X}
1908{% /callout_card %}
1909
1910## 1. Install the package
1911
1912\`\`\`bash
1913npm install ${Y}
1914\`\`\`
1915
1916The recorder must be loaded **before** your application code so it can patch Node.js' HTTP modules before any requests are made.
1917Pick the option below that matches your setup.
1918
1919## 2. Initialize the recorder
1920
1921{% tabs direction="grid" noTabSelectedByDefault=true %}
1922{% tab label="Next.js" %}
1923## Next.js
1924
1925Next.js loads the file named \`instrumentation.ts\` (or \`instrumentation.js\`) at the root of your project before the rest of your app
1926boots. Initialize the recorder from its \`register\` hook, guarding on the Node.js runtime so it never runs in the Edge runtime or the browser:
1927
1928{% code_with_project_selector %}
1929\`\`\`ts
1930// instrumentation.ts
1931export async function register() {
1932  if (process.env.NEXT_RUNTIME === "nodejs") {
1933    const { initBackendRecorder } = await import(
1934      "${Y}"
1935    );
1936    await initBackendRecorder({
1937      meticulousProjectName: "{% project_name /%}",
1938      recordingToken: "{% project_recording_token /%}",
1939    });
1940  }
1941}
1942\`\`\`
1943{% /code_with_project_selector %}
1944
1945Mark the package as an external package so Next.js does not try to bundle it. In \`next.config.js\`:
1946
1947\`\`\`js
1948// next.config.js
1949module.exports = {
1950  serverExternalPackages: ["${Y}"],
1951};
1952\`\`\`
1953
1954If you are using the App Router, also follow the [additional App Router setup](${o.NEXTJS_APP_ROUTER_ADDITIONAL_SETUP_URL}) to ensure
1955Meticulous can correctly test your app.
1956
1957${X}
1958{% /tab %}
1959
1960{% tab label="TanStack Start" %}
1961## TanStack Start
1962
1963TanStack Start's server entry point (\`src/server.ts\` by default) is the first server module that runs, so it's the right place
1964to load the recorder. Create a separate \`src/instrumentation.ts\` file that initializes it, then import that file as the very
1965first import in \`src/server.ts\` — ahead of the \`@tanstack/react-start/server-entry\` import — so the recorder patches Node's
1966HTTP modules before any request-handling code runs:
1967
1968{% code_with_project_selector %}
1969\`\`\`ts
1970// src/instrumentation.ts
1971import { initBackendRecorder } from "${Y}";
1972
1973await initBackendRecorder({
1974  meticulousProjectName: "{% project_name /%}",
1975  recordingToken: "{% project_recording_token /%}",
1976});
1977\`\`\`
1978{% /code_with_project_selector %}
1979
1980\`\`\`ts
1981// src/server.ts
1982import "./instrumentation";
1983
1984import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
1985
1986export default createServerEntry({
1987  fetch(request) {
1988    return handler.fetch(request);
1989  },
1990});
1991\`\`\`
1992
1993If your build ends up bundling \`${Y}\` into the server output, mark it as external in your Vite/Nitro
1994server config so it keeps patching the real Node.js \`http\`/\`https\` modules rather than a bundled copy. ${X}
1995{% /tab %}
1996
1997{% tab label="Node.js (instrumentation file)" %}
1998## Node.js
1999
2000Create an \`instrumentation.js\` file at the root of your project that initializes the recorder:
2001
2002{% code_with_project_selector %}
2003\`\`\`js
2004// instrumentation.js
2005const { initBackendRecorder } = require("${Y}");
2006
2007initBackendRecorder({
2008  meticulousProjectName: "{% project_name /%}",
2009  recordingToken: "{% project_recording_token /%}",
2010});
2011\`\`\`
2012{% /code_with_project_selector %}
2013
2014Start your app with the \`--require\` flag so the recorder is loaded before your application code:
2015
2016\`\`\`bash
2017node --require ./instrumentation.js app.js
2018\`\`\`
2019
2020${X}
2021{% /tab %}
2022
2023{% tab label="Cloudflare Workers" %}
2024## Cloudflare Workers
2025
2026Workers run on the workerd runtime rather than Node.js, so the Node backend recorder above cannot be loaded in-process
2027(skip step 1 — the \`${Y}\` package is not used here). Instead, Meticulous records during local
2028development (\`wrangler dev\`) with a two-part setup:
2029
2030- A lightweight **shim** (\`${J}\`) wraps your Worker's fetch handler and captures inbound requests
2031  plus outgoing \`fetch\` calls. Outgoing requests still go directly to their destination — the recorder is never in the
2032  request path — and when no sidecar is configured the shim is a complete no-op, so it is safe to keep in deployed code.
2033- The **Meticulous recorder sidecar**, a small Node process on your dev machine started by the Meticulous CLI, receives
2034  those events and uploads them to Meticulous as backend recordings.
2035
2036Install the shim and wrap your Worker's handler:
2037
2038\`\`\`bash
2039npm install ${J}
2040\`\`\`
2041
2042\`\`\`ts
2043import { withMeticulous } from "${J}";
2044
2045export default withMeticulous({
2046  async fetch(request, env, ctx) {
2047    // your app
2048  },
2049});
2050\`\`\`
2051
2052Enable the \`nodejs_als\` compatibility flag in your \`wrangler.toml\` (if you already use \`nodejs_compat\` — e.g. for
2053TanStack Start — you're done, it includes it):
2054
2055\`\`\`toml
2056compatibility_flags = ["nodejs_als"]
2057\`\`\`
2058
2059Then run your dev command through the Meticulous CLI, which starts the sidecar and passes its URL to \`wrangler dev\`
2060automatically:
2061
2062\`\`\`bash
2063npx @alwaysmeticulous/cli record backend -- npx wrangler dev
2064\`\`\`
2065
2066Authenticate with \`npx @alwaysmeticulous/cli auth login\` first (or pass \`--apiToken\`). If you prefer to run
2067\`wrangler dev\` yourself, \`npx @alwaysmeticulous/cli record backend\` (without a wrapped command) starts just the
2068sidecar and prints the \`--var METICULOUS_SIDECAR_URL:...\` / \`.dev.vars\` line to point your Worker at it — the value
2069must be a worker var, since host environment variables are not visible inside workerd.
2070
2071\`fetch\` egress is captured (including \`node:http\`/\`node:https\` clients under \`nodejs_compat\`, which are implemented
2072over fetch), as are calls through \`fetch\`-shaped bindings — service bindings and Durable Object stubs — with no code
2073change beyond the \`withMeticulous\` wrapper. Assets bindings are skipped by default, since asset traffic is high-volume
2074and adds nothing to a replay. KV, D1, R2, Queues, RPC method calls on a named entrypoint (\`env.SVC.someMethod()\`), and
2075WebSockets are not yet supported.
2076${X}
2077{% /tab %}
2078
2079{% /tabs %}
2080
2081## 3. Configuration options
2082
2083\`initBackendRecorder\` accepts an optional config object:
2084
2085| Option | Type | Description |
2086|---|---|---|
2087| \`enabled\` | \`boolean\` | Enable or disable the recorder. Defaults to \`true\`. |
2088| \`meticulousProjectName\` | \`string\` | The name of your Meticulous project. |
2089| \`recordingToken\` | \`string\` | Token used to authenticate span uploads. This is the same recording token used by the frontend recorder snippet. |
2090| \`exportMode\` | \`"local" \\| "s3"\` | Where to export recorded spans. Defaults to \`"s3"\`, which uploads to Meticulous. Use \`"local"\` to write sessions to disk for debugging. |
2091| \`localOutputDir\` | \`string\` | Directory for local exports. Only used when \`exportMode\` is \`"local"\`. |
2092| \`flushIntervalMs\` | \`number\` | How often to flush spans, in milliseconds. |
2093| \`spanRedactionHooks\` | \`((value: string, jsonPath: readonly string[]) => string)[]\` | Ordered record-time hooks that transform redactable span strings before they are uploaded. Node.js only. |
2094
2095A common pattern is to record only in the environments you care about:
2096
2097{% code_with_project_selector %}
2098\`\`\`ts
2099await initBackendRecorder({
2100  enabled: process.env.NODE_ENV !== "production",
2101  meticulousProjectName: "{% project_name /%}",
2102  recordingToken: "{% project_recording_token /%}",
2103});
2104\`\`\`
2105{% /code_with_project_selector %}
2106
2107### Redacting recorded backend data
2108
2109Use \`spanRedactionHooks\` to replace sensitive values before completed spans are saved or uploaded. Each hook re
2109ceives every
2110redactable string plus the \`jsonPath\` locating it within the span, and must return a string. Hooks run in the order they are provided:
2111
2112{% code_with_project_selector %}
2113\`\`\`ts
2114await initBackendRecorder({
2115  meticulousProjectName: "{% project_name /%}",
2116  recordingToken: "{% project_recording_token /%}",
2117  spanRedactionHooks: [
2118    (value, jsonPath) =>
2119      jsonPath.at(-1) === "meticulous.prisma.args"
2120        ? redactJsonLeaves(value)
2121        : value.replace(
2122            /api_key=[A-Za-z0-9_-]+/g,
2123            "api_key=[REDACTED_API_KEY]",
2124          ),
2125  ],
2126});
2127
2128// Only a string leaf can hold a secret: a where: { apiKey: { ... } } relation
2129// filter shares the name but is structure, so the type check leaves it intact.
2130function redactJsonLeaves(json: string): string {
2131  return JSON.stringify(JSON.parse(json), (key, value) =>
2132    key === "apiKey" && typeof value === "string" ? "[REDACTED]" : value,
2133  );
2134}
2135\`\`\`
2136{% /code_with_project_selector %}
2137
2138The hooks cover span names, error/status messages, and all span attribute values, including strings inside arrays or objects and JSON
2139captured inside strings. They do not transform attribute names, trace/span/parent IDs, timestamps, span kind, client-technology routing,
2140or frontend session IDs.
2141
2142Hooks run only while recording. If a hook throws or returns a non-string, Meticulous abandons the recording rather than saving the span
2143without redaction. Replacing backend-generated request data can also change a replay match key; use stable replacements and verify that
2144the corresponding input will have the same value during replay. This programmatic option applies to the Node.js recorder and is not
2145available to the Cloudflare Workers sidecar.
2146
2147Many redactable strings are serialized JSON — database query arguments and results, and request and response bodies. Rewriting those with
2148text substitutions risks emitting something that no longer parses: a pattern matching \`"apiKey":\` followed by an unquoted run will
2149consume the \`{\` of an object value, and replacing a number or boolean with an unquoted placeholder is invalid JSON too. Meticulous
2150matches database mocks on the serialized arguments, so a recording that no longer parses stops matching exactly and falls back to looser
2151tiers, which can serve another query's result. Prefer parsing the JSON, redacting the leaf values, and re-serializing — use \`jsonPath\`
2152to recognise those attributes — and return the input unchanged when a hook redacts n
2152othing, so unaffected bodies keep their exact bytes.
2153
2154## 4. Flush spans on shutdown
2155
2156\`initBackendRecorder\` returns a handle with a \`stopRecording()\` method. Call it before your process exits so any pending spans are
2157flushed and uploaded:
2158
2159\`\`\`ts
2160const handle = await initBackendRecorder({
2161  /* ...config... */
2162});
2163
2164process.on("SIGTERM", async () => {
2165  await handle?.stopRecording();
2166  process.exit(0);
2167});
2168\`\`\`
2169
2170## 5. Recording anything else
2171
2172The recorder instruments the common clients automatically — \`fetch\`, \`http\`, Postgres, Prisma, Redis. For anything else, wrap the call
2173yourself:
2174
2175\`\`\`ts
2176const user = await handle.withMeticulousOperation(
2177  { name: "crm.getUser", key: { id } },
2178  () => crm.getUser(id),
2179);
2180\`\`\`
2181
2182While recording, Meticulous runs your function and captures what it returned. During a replay it does **not** run it — it returns the
2183recorded result (or throws the recorded error) in its place. That is why the wrapper has to make the call rather than be told about it
2184afterwards.
2185
2186There are two good reasons to reach for this.
2187
2188The first is a client we don't instrument — a gRPC stub, a vendor SDK with its own transport.
2189
2190The second is more interesting, and applies even to calls we *do* instrument: **an operation that sits above the network**. Take a
2191function that checks an in-process cache and only calls an API on a miss. If the cache was warm while recording there was no request to
2192record, so nothing is captured and the replay has nothing to serve. Wrap the function instead and the recording holds the operation
2193itself — so it replays whether or not the cache happened to be warm, and cache hits stop making replays inconsistent.
2194
2195\`\`\`ts
2196const getUser = (id: string) =>
2197  handle.withMeticulousOperation({ name: "users.get", key: { id } }, async () => {
2198    const cached = cache.get(id);
2199    if (cached) return cached;
2200    const user = await api.fetchUser(id);
2201    cache.set(id, user);
2202    return user;
2203  });
2204\`\`\`
2205
2206A few things to know:
2207
2208- **\`name\` identifies the operation, so renaming it invalidates existing recordings.** A test run compares against a base recorded days
2209  or weeks earlier, so after a rename every call to that operation has nothing to match and the request fails. Rename deliberately.
2210- **\`key\` is what distinguishes one call from another** — usually the arguments. Leave out values that change on every call but don't
2211  affect the result, such as a request id or a nonce; including them means no call ever matches its recording. Timestamps and UUIDs are
2212  handled for you, and if a key still doesn't match, Meticulous falls back to a recording of the same operation.
2213- **Arguments and results are stored as JSON**, so a \`Date\` comes back as a string and a \`Map\` as \`{}\`. Meticulous logs a warning naming
2214  the exact field when it sees one while recording. Thrown errors are captured and re-thrown with their \`name\`, \`message\` and custom
2215  properties intact, though \`instanceof\` checks against your own error class won't match.
2216- **Synchronous functions stay synchronous** on both paths.
2217- **A call with no recording fails the request** rather than quietly running for real — a replay that reaches live services isn't
2218  reproducible.
2219
2220To record app state that no call produces — resolved feature flags, a chosen experiment arm — use:
2221
2222\`\`\`ts
2223handle.recordMeticulousObservation("featureFlags.resolved", flags);
2224\`\`\`
2225
2226This only records: it never stubs anything, never throws, and is ignored during replay.
2227
2228### If you'd rather not hand us the call
2229
2230Some teams don't want their own code running inside our callback. The same capture is available as two calls you make yourself, with the
2231branch in your code:
2232
2233\`\`\`ts
2234function getUser(id: string) {
2235  if (handle.isMeticulousReplaying()) {
2236    return handle.stubWithMeticulous<User>(\`user_\${id}\`);
2237  }
2238
2239  const user = crm.getUser(id);
2240  handle.recordWithMeticulous(\`user_\${id}\`, user);
2241
2242  return user;
2243}
2244\`\`\`
2245
2246\`recordWithMeticulous\` takes the value the operation produced. A promise is fine, and is the usual case: its resolved value is recorded, the
2247promise you return is untouched, and \`stubWithMeticulous\` then returns a promise to match. These are the same recordings
2248\`withMeticulousOperation\` produces, so you can move between the two forms without invalidating anything. Here the name is the whole
2249identity — there is no separate \`key\`, so put whatever distinguishes one call from another into the name.
2250
2251Use \`isMeticulousReplaying()\` for the branch rather than checking an env var yourself. The mode a process was started in, and an image
2252built for Meticulous, are both the same in either mode, so neither tells you whether a recorded outcome can actually be served.
2253
2254Two things you give up by splitting it, which is why wrapping is still the better default where it's acceptable:
2255
2256- **A thrown error isn't captured.** \`recordWithMeticulous\` is handed a value, so a call that threw never reaches it and the replay has
2257  nothing to serve. A rejected promise *is* captured. If failure is part of the flow, wrap instead.
2258- **The branch is yours to get right**, and only the replay side of it is exercised by a replay — so a mistake in the other side won't
2259  show up until it reaches production.
2260
2261That's it — once your app is running with the backend recorder enabled, server-side requests will be captured alongside the frontend
2262sessions and used to stub backend calls during replay.
2263`,Z=`---
2264{
2265  "title": "Architecture Overview"
2266}
2267---
2268
2269# {% $frontmatter.title %}
2270
2271Understand how Meticulous works at a high level - from recording user sessions to detecting visual differences in your pull requests.
2272
2273---
2274
2275## The Big Picture
2276
2277Meticulous automates end-to-end testing by:
2278
22791. **Recording** real user interactions in your application
22802. **Selecting** the most valuable sessions for testing
22813. **Replaying** those sessions on every code change
22824. **Comparing** screenshots to detect visual differences
2283
2284Think of it as "TiVo for your application" - recording how users interact with your app, then replaying those interactions to catch bugs.
2285
2286---
2287
2288## The Four Phases
2289
2290### 1. Recording Phase
2291
2292**What happens**: As users interact with your application, Meticulous captures everything they do.
2293
2294**What gets recorded**:
2295- Clicks, typing, scrolling, navigation
2296- Network requests and their responses
2297- What the page looked like at key moments
2298
2299**Where this happens**: Wherever you install the recorder (localhost, staging, production)
2300
2301**The key idea**: Real users create your tests just by using your app normally.
2302
2303---
2304
2305### 2. Selection Phase
2306
2307**What happens**: Meticulous analyzes all recorded sessions and picks the best ones for testing.
2308
2309**The goal**: Get maximum coverage with minimal redundancy. Instead of running thousands of sessions, run the 200-500 that matter most.
2310
2311**How sessions are chosen**:
2312- Do they visit unique pages?
2313- Do they exercise different user flows?
2314- Are they recent (newer sessions preferred)?
2315- Do they provide good coverage?
2316
2317**The result**: A "golden set" of sessions that represent your app's core functionality.
2318
2319---
2320
2321### 3. Replay Phase
2322
2323**What happens**: When you create a pull request, Meticulous replays your golden set against both versions of your app.
2324
2325**The process**:
2326
23271. **Build your app** from the PR code
23282. **Replay each session** - simulating the exact same user interactions
23293. **Take screenshots** at important moments
23304. **Compare with baseline** - screenshots from your main branch
2331
2332**The magic**: Network requests are "stubbed" - Meticulous replays the recorded API responses, so you don't need your backend running.
2333
2334**Two test runs**:
2335- **Base run**: How your app looked on the main branch
2336- **Head run**: How your app looks with your changes
2337
2338---
2339
2340### 4. Integration Phase
2341
2342**What happens**: Results are posted back to your pull request.
2343
2344**You get**:
2345- A comment showing which screenshots changed
2346- A status check (pass/fail)
2347- A link to review differences visually
2348
2349**What you do**: Review the diffs and either:
2350- Approve them (if changes are intentional)
2351- Fix the bug (if something broke)
2352- Investigate false positives
2353
2354---
2355
2356## Key Concepts Explained
2357
2358### Network Stubbing: Testing Without a Backend
2359
2360**The problem**: Traditional E2E tests need your entire stack running - database, backend, third-party APIs. This is slow and brittle.
2361
2362**Meticulous' solution**: Record API responses once, replay them forever.
2363
2364**How it works**:
2365
2366**During recording**:
2367- User clicks "Login"
2368- Browser sends request to your API
2369- API responds with user data
2370- Meticulous saves both the request and response
2371
2372**During replay** (weeks later, no backend needed):
2373- Test clicks "Login"
2374- Browser tries to send the same request
2375- Meticulous intercepts it and returns the saved response
2376- Browser never knows the difference!
2377
2378**Why this matters**:
2379- Tests run faster (no real API calls)
2380- Tests are deterministic (same input, same output)
2381- Tests are zero-effort to set up (no backend infrastructure needed)
2382- Edge cases are preserved (error responses replay exactly as recorded)
2383
2384For how stubs stay useful when APIs drift, mutation-then-GET flows, and why stubs don't need to be perfect, see [Network Recording & Patching](${o.NETWORK_RECORDING_AND_PATCHING_URL}).
2385
2386---
2387
2388### Sessions, Test Runs, and Replays
2389
2390**Session**: One user's journey through your app
2391
2392*Example*: User visits homepage → clicks product → adds to cart → checks out
2393
2394**Test Run**: All tests for one commit
2395
2396*Contains*: Multiple replays (one per selected session) for a specific version of your code
2397
2398**Replay**: Playing back one session
2399
2400*Result*: Series of screenshots showing what happened
2401
2402---
2403
2404### Base vs Head: How Diffs Are Detected
2405
2406**Base commit**: Your main branch (the "before" state)
2407
2408**Head commit**: Your PR branch (the "after" state)
2409
2410**How comparison works**:
24111. Run tests on base commit → get baseline screenshots
24122. Run tests on head commit → get new screenshots
24133. Compare them pixel-by-pixel
24144. Report any differences
2415
2416**Why you need both**: Without a baseline, there's nothing to compare against. The first PR after setup has no base, so it just establishes one.
2417
2418---
2419
2420## How Sessions Become Tests
2421
2422Let's follow a session from recording to diff detection:
2423
2424**Day 1 - Recording**:
2425- Sarah (your user) visits your app
2426- She searches for "blue shoes", clicks a result, adds to cart
2427- Meticulous records: every click, every API response, every screenshot
2428
2429**Day 2 - Selection**:
2430- Meticulous analyzes Sarah's session
2431- It covers the search page, product page, and cart - good coverage!
2432- Session added to the golden set
2433
2434**Day 7 - You Create a PR**:
2435- You change the product page layout
2436- CI runs Meticulous tests
2437- Sarah's session replays on both main branch and your PR branch
2438
2439**Comparison**:
2440- Product page screenshot on main: Shows old layout
2441- Product page screenshot on PR: Shows your new layout
2442- **Diff detected!**
2443
2444**Your action**:
2445- Review the diff
2446- Looks good, this was intentional
2447- Click "Approve"
2448- PR can now be merged
2449
2450---
2451
2452## Deployment Types
2453
2454### Option 1: Upload Static Assets (Recommended for static sites)
2455
2456**When to use**: Pure static site (HTML/JS/CSS, no server-side rendering)
2457
2458**How it works**:
2459- CI builds your static files
2460- Meticulous uploads them to cloud storage
2461- Tests run against the hosted static site
2462
2463**Pros**: Simplest and most reliable setup
2464
2465---
2466
2467### Option 2: Upload a Docker Container (Recommended for server-rendered apps)
2468
2469**When to use**: Server-rendered apps (Next.js, Nuxt, etc.)
2470
2471**How it works**:
2472- CI builds a Docker image of your app
2473- Meticulous hosts and runs the container
2474- Tests run against the containerized app
2475
2476**Pros**: Works with any server-rendered framework, reliable
2477
2478---
2479
2480### Option 3: Preview URLs
2481
2482**When to use**: You deploy to preview URLs (Vercel, Netlify, etc.)
2483
2484**How it works**:
2485- Your deployment service creates a preview URL
2486- Meticulous tests directly against that URL
2487- No CI workflow changes needed
2488
2489**Pros**: No build step in CI, tests the actual deployed version
2490
2491---
2492
2493## Common Questions
2494
2495### "Do I need my backend running during tests?"
2496
2497**No!** That's the whole point of network stubbing. API responses are replayed from recordings.
2498
2499### "What if my backend changes?"
2500
2501Tests still pass as long as your *frontend* works correctly. Backend changes don't affect frontend tests.
2502
2503### "What if I change an API response format?"
2504
2505Meticulous patches affected sessions using newer recordings of the same endpoint shape when possible, and replaces sessions that no longer add unique coverage. See [Network Recording & Patching](${o.NETWORK_RECORDING_AND_PATCHING_URL}).
2506
2507### "How does Meticulous know what changed?"
2508
2509Pixel-by-pixel screenshot comparison. If even one pixel differs, it's flagged as a diff.
2510
2511### "Can't I just approve all diffs and move on?"
2512
2513You could, but then you'd miss real bugs! The point is to catch unintended visual changes.
2514
2515---
2516
2517## What Meticulous Tests
2518
2519**Tests**:
2520- How your UI looks
2521- How user interactions work
2522- What users see after clicking buttons
2523- Visual regressions (layout shifts, styling bugs)
2524- Functional bugs (broken navigation, missing elements)
2525- Business logic in the frontend (e.g. pricing calculations, discount logic)
2526
2527**Doesn't test**:
2528- Backend logic (we recommend testing backend logic in unit and integration tests)
2529- Cross-browser compatibility (tests run in Chrome)
2530- Accessibility (though visual review can help)
2531
2532---
2533
2534## Why This Architecture?
2535
2536**Goal**: Make E2E testing so easy that teams actually use it.
2537
2538**Challenges with traditional E2E tests**:
2539- Slow (wait for backend, database, APIs)
2540- Flaky (network issues, timing problems)
2541- Expensive (infrastructure costs)
2542- Hard to maintain (tests break when UI changes and backend logic changes)
2543
2544**How Meticulous solves these**:
2545- **Fast**: Network stubbing removes backend dependency
2546- **Deterministic**: Recorded responses = same results every time
2547- **Low maintenance**: Tests are user sessions, not code to update
2548
2549---
2550
2551## Summary
2552
2553Meticulous works by:
2554
25551. 📹 **Recording** real user sessions (clicks, API calls, screenshots)
25562. 🎯 **Selecting** the best sessions for comprehensive coverage
25573. ▶️ **Replaying** sessions on every PR (with API responses stubbed)
25584. 🔍 **Comparing** screenshots to detect visual differences
2559
2560**The insight**: Let users create your tests. You just record what they do and replay it.
2561
2562**The innovation**: Network stubbing makes tests fast and deterministic without needing your backend.
2563
2564**The result**: Catch bugs before they reach production, with minimal effort.
2565`,ee=`---
2566{
2567  "title": "Network Recording & Patching"
2568}
2569---
2570
2571# {% $frontmatter.title %}
2572
2573How Meticulous records network traffic, stubs it during replay, and keeps sessions useful as your frontend and APIs evolve.
2574
2575---
2576
2577## The short version
2578
2579Meticulous tests your **frontend**, not your backend.
2580
2581When a session is recorded, we store the user events **and** the network request/response pairs from that walkthrough. On each PR we replay the sessions that exercise your changes against your new frontend build with **no live backend**: each request the browser makes is intercepted and answered from the recording.
2582
2583That raises an obvious question: *what happens when APIs change, or when the "same" GET returns different data depending on earlier mutations?* This page explains how that works - and why getting every stub perfectly right is **not** what makes Meticulous valuable.
2584
2585---
2586
2587## What a recording actually contains
2588
2589A session is **not** a set of screenshots. It is roughly:
2590
25911. **User events** - clicks, keystrokes, scrolls, and so on
25922. **Browser state** - cookies, local/session storage, viewport, and related metadata
25933. **Network traffic** - every XHR/fetch (and related) request and response body from that session
2594
2595Screenshots are taken later, at **replay** time, against whatever frontend build you give us.
2596
2597So the original recording is a frozen walkthrough: at this point we clicked X; the app then issued these requests and got these responses.
2598
2599---
2600
2601## What happens on a PR (no backend required)
2602
2603When CI sends us your frontend assets (or a container / preview URL):
2604
26051. We spin up your app in our deterministic browser
26062. We evaluate the [selected sessions](${o.TESTING_POOL_URL}) and replay the flows that exercise your PR's code changes - skipping flows that don't reach the diff, or that only repeat coverage another flow already provides
26073. When the app makes a network call, we **stub** it from that session's recorded traffic
26084. We take screenshots whenever the UI changes, on both the base and head commits
26095. We show you every visual / behavioral difference
2610
2611Because network timing and response bodies are controlled, before/after screenshots line up and flake rates stay extremely low.
2612
2613---
2614
2615## Mutation-then-GET flows
2616
2617A common pattern in complex apps:
2618
2619> A user creates a record, fills fields, submits (mutations), then hits a GET for "latest copy of this data." The GET URL looks the same every time, but the response depends on what happened earlier in the flow.
2620
2621**Within a single recorded session, this works cleanly.**
2622
2623That session's network recording contains:
2624
2625- the create/update/submit calls **and** their responses, in order
2626- the later GET **and** the specific response that followed those mutations
2627
2628On replay we do **not** re-hit a live backend to recreate state. We stub the whole chain. Sequence matters: the Nth matching GET in the recording is returned for the Nth matching GET at replay time. The mutations don't need to "really" mutate anything - their recorded responses are what put the frontend into the right state for the rest of the flow.
2629
2630So for *"someone walked through this setup once while developing"*: that exact walkthrough, with that exact data shape and UI state, stays available to re-test on PRs that exercise that path for as long as that session stays in the golden set.
2631
2632You do **not** need a developer to manually re-run hundreds of permutations later. You need the recorder to have seen each distinct UI path **once**. Session selection keeps the ones that still contribute unique coverage.
2633
2634---
2635
2636## How we tell similar requests apart
2637
2638There are two different matching problems:
2639
2640### Inside one session (replay stubbing)
2641
2642Matching is **sequence-aware**. Two GETs to the same endpoint with the same shape are not collapsed into one response - we consume recorded entries in order. That preserves mutation → GET causality inside a walkthrough.
2643
2644We also normalize things that change between environments (preview hostname vs recorded hostname, dynamic path IDs like \`/applications/123\` → \`/applications/{id}\`, GraphQL operation name + field selection, and so on) so the same logical call still matches.
2645
2646### Across sessions (keeping old sessions alive when APIs drift)
2647
2648Separately, we maintain a project-wide **pool of recent request fingerprints → responses** from newer recordings.
2649
2650A fingerprint is keyed on things like:
2651
2652- HTTP method
2653- Normalized path (dynamic segments generalized)
2654- For GraphQL: operation name(s), variable **names**, selected fields (not variable *values*)
2655- For other JSON POSTs: top-level body keys (not values)
2656
2657**Values are intentionally ignored in the fingerprint.** The point of cross-session matching is "same endpoint / same schema shape," not "same invoice ID." That is a deliberate tradeoff - see [Why 
2657stubs don't need to be perfect](#why-stubs-dont-need-to-be-perfect).
2658
2659When a PR's frontend starts requesting a slightly different shape (new GraphQL field, renamed key, new endpoint), the old session's recorded responses can become stale. We then:
2660
26611. Detect network mismatch / divergence on the original replay
26622. Look up a newer donor response with the same fingerprint
26633. Prefer **schema-level patching**: update the response *shape* while keeping the original session's primitive values where possible
26644. Re-run the affected sessions with the patched recording
26655. Only keep the patched result if it is actually better (fewer console errors / cleaner screenshots)
2666
2667If a session becomes fully obsolete and no longer adds unique code coverage, [session selection](${o.TESTING_POOL_URL}) replaces it with a newer recording that does.
2668
2669If there is no good donor yet, we have fallbacks. In practice, for an active engineering org, new developer and user sessions continuously refill the pool - especially right after an API change, when people are testing the new frontend against the new backend.
2670
2671---
2672
2673## Coverage for flows nobody has touched in months
2674
2675Meticulous is **not** "continuously re-running only the tests someone thought to write this sprint."
2676
2677The model is:
2678
26791. Someone goes through a flow **once** with the recorder on - including obscure settings pages and edge states
26802. That session is mapped to the lines of code it executed
26813. If those lines aren't covered better by another session, it stays in the golden set
26824. PRs that touch those lines re-execute it against the new frontend, with its recorded network traffic (patched over time as schemas evolve)
2683
2684So coverage of "tests we didn't think about" comes from **having seen the UI once**, not from someone remembering to maintain a Playwright or Cypress case for it.
2685
2686What we are *not* claiming: that we magically invent backend states nobody has ever produced. If a UI state has never been reached in a recorded environment, we can't replay it. In practice, large products accumulate a lot of those states quickly (dev, staging, internal dogfood), and selection keeps the rare ones.
2687
2688---
2689
2690## Why stubs don't need to be perfect
2691
2692### What you are optimizing for
2693
2694Meticulous answers: **"If I merge this PR, what will change in the UI - including pages and states I didn't think to check?"**
2695
2696It does **not** answer: **"Is the backend returning the correct business data for record #48291?"** Backend correctness stays with your API and contract tests.
2697
2698Holding network data "close enough" is enough to isolate **frontend** regressions: layout, components, client-side logic, broken conditionals, wrong empty states, permission-denied UI, and so on.
2699
2700### Analogy
2701
2702Playwright tests with fixtures or MSW mocks also don't use live production data for every case. You still catch UI bugs. Meticulous is the same idea, except the fixtures are harvested automatically from real sessions and refreshed automatically when schemas drift.
2703
2704### Why imperfect stubs rarely hide the bugs that matter
2705
2706| Situation | What happens |
2707|-----------|----------------|
2708| Frontend CSS/component change | Screenshots differ even if the API payload is slightly off |
2709| Frontend logic change (error path, disabled button, wrong branch) | Behavior/screenshots differ under the recorded responses |
2710| Stub is badly wrong | Often shows as console errors, blank/error UI, or **network divergence** indicators - not a silent green |
2711| Stub is slightly wrong but unused fields | No visual diff - fine; those fields weren't part of the UI under test |
2712| Schema drift from a real API change | Patching + dual-run merge prefers the result that actually renders cleanly |
2713
2714A bad stub that makes a page explode is **visible**. A perfect stub of an invoice amount you never render does not help catch a broken "Create" button.
2715
2716### Breadth beats perfect fidelity for this class of bug
2717
2718The common pain is not the tests teams already think about - it's the cases they don't realize they're affecting.
2719
2720That problem is solved by **replaying many real UI paths automatically**, not by guaranteeing that every stubbed GET returns the exact same row as production would today. One slightly imperfect recording of an obscure settings page that nobody wrote an E2E for is more valuable than a perfect mock of a flow you already test manually.
2721
2722Backend / data-correctness gaps are real - they are just **out of scope** for a frontend visual/behavioral regression system, the same way a UI E2E against an ephemeral environment doesn't replace unit tests for interest-calculation logic.
2723
2724---
2725
2726## How this fits with the rest of your tests
2727
2728| Layer | Job |
2729|-------|-----|
2730| Unit / integration / API / contract tests | Logic, services, and backend correctness |
2731| **Meticulous** | Exhaustive frontend blast-radius on every PR, without spinning backends, without writing or maintaining UI tests |
2732
2733Meticulous covers the UI regression and blast-radius problem that hand-written E2Es are usually meant to solve: catching broken screens, flows, and states across the app on every PR. Because we only need the frontend build, you also avoid the cost of spinning every dependent service for that verification.
2734
2735---
2736
2737## Concrete lifecycle example
2738
27391. **Month 0** - An engineer walks through "create record → fill → submit → view status" on staging. The recorder captures events and all network pairs.
27402. **Month 0** - Session selection puts it in the golden set (it covers unique UI code).
27413. **Month 3** - A PR renames a GraphQL field the status page queries. The original recording is stale → network divergence → we patch the response shape from a newer recording of that operation → re-run → the merged result shows the real UI impact of the rename (or a clean bill of health).
27424. **Month 8** - A newer session covers the same lines more efficiently; the old one ages out. Coverage continues; stubs are fresher.
2743
2744No one had to rewrite a test. No one had to re-walk hundreds of permutations. The original walkthrough kept protecting that UI until something better replaced it.
2745
2746---
2747
2748## What is and isn't stubbed
2749
2750**Stubbed by Meticulous:**
2751
2752- XHR (XMLHttpRequest) requests
2753- Fetch API requests
2754- WebSocket connections
2755- Local storage, session storage, and cookies
2756
2757**Not stubbed by Meticulous:**
2758
2759- Static assets (CSS, JavaScript, images) loaded directly by the browser via HTML tags
2760- Assets referenced with absolute URLs in your HTML (for example, \`<script src="https://example.com/app.js">\`)
2761
2762Static assets are loaded live from whatever URL they're referenced at. Prefer relative URLs (for example, \`/dist/app.js\`) so assets load correctly across test environments.
2763
2764If you wish to test backend code with Meticulous, you can choose which subset of requests to stub in the **Network Stubbing** tab in your project settings. For Next.js App Router apps, the default is to stub all requests apart from server-component and static-asset requests.
2765
2766---
2767
2768## Limitations
2769
2770- We test **frontend rendering and client behavior** under recorded (and patched) API traffic - not live backend correctness.
2771- Cross-session donors can come from a **different user/app state** with the same request shape. Heuristics and conservative merge reduce damage; they don't make it impossible.
2772- A UI state that has **never** been recorded cannot be replayed.
2773- For hard cases we have additional repair fallbacks; the best way to evaluate quality for your app is to run Meticulous on real PRs.
2774
2775---
2776
2777## Summary
2778
2779| Concern | Answer |
2780|---------|--------|
2781| How do mutation → GET flows work? | Whole chain is recorded and sequence-stubbed inside that session. No live backend needed to recreate state. |
2782| How do stubs stay fresh? | Project-wide response pool + schema-preserving patches + golden-set replacement of stale sessions. |
2783| How do identical-looking requests differ? | Inside a session: order. Across sessions: fingerprint is shape (path/op/fields), not values. |
2784| Coverage for forgotten flows? | Record once → stays selected while it adds unique coverage → replayed when a PR touches that path. |
2785| Must stubs be perfect? | No. Signal is UI blast radius. Bad stubs tend to surface loudly; perfect backend data is a different testing layer. |
2786`,et=`---
2787{
2788  "title": "Glossary"
2789}
2790---
2791
2792# {% $frontmatter.title %}
2793
2794Alphabetical reference of Meticulous terminology and concepts.
2795
2796---
2797
2798## API Token
2799
2800**Definition**: A secret authentication token used to authenticate Meticulous API requests.
2801
2802**Where used**: CI workflows, CLI commands
2803
2804**How to get**: From the Meticulous dashboard project settings
2805
2806**Security**: Should be stored as a CI secret (e.g., \`METICULOUS_API_TOKEN\`)
2807
2808**Related concepts**: [Project](#project)
2809
2810**Related docs**: GitHub Actions setup
2811
2812---
2813
2814## Base Commit
2815
2816**Definition**: The commit from your main/target branch that a PR is based on.
2817
2818**Purpose**: Provides the comparison point for detecting diffs. The **base test run** shows how the app looked before your changes.
2819
2820**Example**:
2821- Main branch is at commit \`abc123\`
2822- You create a PR from commit \`abc123\`
2823- Base commit = \`abc123\`
2824- Head commit = Your latest PR commit
2825
2826**Related concepts**: [Head Commit](#head-commit), [Base Test Run](#base-test-run), [Diff](#diff)
2827
2828**Related docs**: Architecture overview
2829
2830---
2831
2832## Base Test Run
2833
2834**Definition**: A test run executed on the **base commit** (main branch).
2835
2836**Purpose**: Serves as the comparison baseline for detecting visual changes in a PR.
2837
2838**When created**:
2839- Automatically on pushes to main branch
2840- Manually via \`workflow_dispatch\`
2841
2842**Why it matters**: Without a base test run, Meticulous cannot detect diffs (no comparison point).
2843
2844**Common issue**: "No base test run found" - occurs when main branch hasn't run yet after adding Meticulous.
2845
2846**Related concepts**: [Head Test Run](#head-test-run), [Base Commit](#base-commit), [Test Run](#test-run)
2847
2848**Related docs**: FAQ and troubleshooting
2849
2850---
2851
2852## Cloud Compute
2853
2854**Definition**: Meticulous execution mode where tests run in Meticulous' cloud infrastructure using a secure tunnel or preview URL.
2855
2856**Use cases**:
2857- Testing locally-served apps via secure tunnel
2858- Testing preview URLs (Vercel, Netlify)
2859- Next.js and server-rendered applications
2860
2861**GitHub Action**: \`alwaysmeticulous/report-diffs-action/cloud-compute@v1\`
2862
2863**Alternative**: [Upload Assets](#upload-assets)
2864
2865**Related concepts**: [Secure Tunnel](#secure-tunnel), [Preview URL](#preview-url)
2866
2867**Related docs**: GitHub Actions setup
2868
2869---
2870
2871## Cloud Replay
2872
2873**Definition**: Testing mode where Meticulous connects directly to a preview URL without a secure tunnel.
2874
2875**Use cases**: Apps deployed to Vercel, Netlify, or other preview URL providers
2876
2877**Advantages**: Faster than tunnel, tests real deployment environment
2878
2879**Configuration**: Requires preview URL integration
2880
2881**Related concepts**: [Preview URL](#preview-url), [Cloud Compute](#cloud-compute)
2882
2883**Related docs**: Cloud replay guide
2884
2885---
2886
2887## Companion Assets
2888
2889**Definition**: Static files uploaded alongside your app and served directly by Meticulous instead of proxying through the tunnel.
2890
2891**Use cases**:
2892- Next.js \`/_next/static/\` folders
2893- Large static assets (images, fonts, videos)
2894- Assets on CDN during recording but local during testing
2895
2896**Configuration**: Requires both:
2897- \`companion-assets-folder\`: Path to local folder
2898- \`companion-assets-regex\`: Regex pattern to match requests
2899
2900**Example**:
2901\`\`\`yaml
2902companion-assets-folder: "companion-assets"
2903companion-assets-regex: "^/_next/static/"
2904\`\`\`
2905
2906**Related concepts**: [Secure Tunnel](#secure-tunnel), [Static Assets](#static-assets)
2907
2908**Related docs**: Companion assets advanced guide
2909
2910---
2911
2912## Custom Event API
2913
2914**Definition**: Advanced Meticulous API for recording and replaying custom events with fine-grained control.
2915
2916**Use cases**:
2917- Complex scenarios beyond custom values API
2918- Timing-sensitive event replay
2919- Custom integration logic
2920
2921**Related concepts**: [Custom Values API](#custom-values-api)
2922
2923**Related docs**: Custom event API guide
2924
2925---
2926
2927## Custom Values API
2928
2929**Definition**: Meticulous API for storing custom data during recording and retrieving it during replay.
2930
2931**Use cases**:
2932- File upload handling (storing file contents)
2933- Feature flag values
2934- User context
2935- Dynamic configuration
2936
2937**Size limits**:
2938- Development: 20MB per value
2939- Production: 1MB per value
2940
2941**API methods**:
2942- \`window.Meticulous.recordCustomValues({})\`
2943- \`window.Meticulous.getCustomValues()\`
2944
2945**Related concepts**: [Custom Event API](#custom-event-api), [File Upload](#file-upload)
2946
2947**Related docs**: Record custom values, Handle file uploads
2948
2949---
2950
2951## Diff
2952
2953**Definition**: A detected visual difference between base and head test runs.
2954
2955**How detected**: Pixel-by-pixel screenshot comparison
2956
2957**States**:
2958- **Unapproved**: Detected, not reviewed
2959- **Approved**: Reviewed and accepted as expected
2960- **Rejected**: Identified as a bug to fix
2961
2962**Workflow**:
29631. Diff detected in test run
29642. Posted to PR comment
29653. Developer reviews in Meticulous UI
29664. Developer approves or rejects
2967
2968**Related concepts**: [Base Test Run](#base-test-run), [Head Test Run](#head-test-run), [Screenshot](#screenshot)
2969
2970**Related docs**: Reviewing diffs
2971
2972---
2973
2974## File Upload
2975
2976**Definition**: Handling of file input elements and drag-and-drop uploads in Meticulous tests.
2977
2978**Challenge**: Meticulous doesn't store uploaded files by default.
2979
2980**Solutions**:
29811. **Skip validation** (recommended): Use \`window.Meticulous.isRunningAsTest\` to bypass file validation
29822. **Store contents**: Use custom values API for small files
29833. **Custom events**: For complex scenarios
2984
2985**Related concepts**: [Custom Values API](#custom-values-api), [Network Stubbing](#network-stubbing)
2986
2987**Related docs**: Handle file uploads
2988
2989---
2990
2991## Golden Set
2992
2993**Definition**: The curated subset of recorded sessions selected for testing. Also called **Selected Sessions**.
2994
2995**Selection criteria**:
2996- Code coverage
2997- Page coverage
2998- User flow diversity
2999- Recency
3000
3001**Typical size**: 200-500 sessions
3002
3003**Updates**: Automatically refreshed as new sessions are recorded
3004
3005**Related concepts**: [Session](#session), [Session Selection](#session-selection)
3006
3007**Related docs**: Architecture overview
3008
3009---
3010
3011## Head Commit
3012
3013**Definition**: The latest commit in a PR branch being tested.
3014
3015**Purpose**: The **head test run** shows how the app looks with your PR changes.
3016
3017**Related concepts**: [Base Commit](#base-commit), [Head Test Run](#head-test-run)
3018
3019**Related docs**: Architecture overview
3020
3021---
3022
3023## Head Test Run
3024
3025**Definition**: A test run executed on the **head commit** (PR branch).
3026
3027**Purpose**: Shows how the app looks with your changes. Compared against base test run to detect diffs.
3028
3029**When created**: On every PR commit
3030
3031**Related concepts**: [Base Test Run](#base-test-run), [Head Commit](#head-commit), [Test Run](#test-run)
3032
3033**Related docs**: Architecture overview
3034
3035---
3036
3037## Network Stubbing
3038
3039**Definition**: Technique where recorded network requests/responses are replayed instead of making real network calls.
3040
3041**How it works**:
3042- During recording: Capture request + response
3043- During replay: Intercept request → Return recorded response
3044- Over time: Patch stale response shapes from newer recordings, and replace sessions that no longer add unique coverage
3045
3046**Benefits**:
3047- No backend needed during tests
3048- Deterministic behavior
3049- Faster test execution
3050
3051**What's stubbed**: All HTTP/HTTPS requests from browser
3052
3053**What's not stubbed**: WebSockets (unless configured), excluded domains
3054
3055**Related concepts**: [Replay](#replay), [Session](#session)
3056
3057**Related docs**: [Network Recording & Patching](${o.NETWORK_RECORDING_AND_PATCHING_URL}), Architecture overview, FAQ
3058
3059---
3060
3061## Preview URL
3062
3063**Definition**: A unique URL generated by deployment platforms (Vercel, Netlify) for each PR.
3064
3065**Use with Meticulous**: Cloud replay can test preview URLs directly without a tunnel.
3066
3067**Advantages**: Faster than tunnel, tests real deployment
3068
3069**Related concepts**: [Cloud Replay](#cloud-replay), [Secure Tunnel](#secure-tunnel)
3070
3071**Related docs**: Cloud replay guide
3072
3073---
3074
3075## Project
3076
3077**Definition**: A Meticulous project represents a single application being tested.
3078
3079**Contains**:
3080- API token for authentication
3081- Recorded sessions
3082- Selected sessions (golden set)
3083- Test runs
3084- Configuration settings
3085
3086**One project per app**: If you have multiple apps, create multiple projects.
3087
3088**Related concepts**: [API Token](#api-token), [Session](#session)
3089
3090**Related docs**: Getting started
3091
3092---
3093
3094## Recorder
3095
3096**Definition**: JavaScript snippet injected into your app that captures user sessions.
3097
3098**Installation methods**:
30991. Script tag in HTML
31002. NPM dependency
3101
3102**What it captures**:
3103- User interactions (clicks, typing, scrolling)
3104- Network requests and responses
3105- DOM snapshots
3106- Page metadata
3107
3108**When active**: During user sessions on production/staging
3109
3110**Related concepts**: [Session](#session), [Session Recording](#session-recording)
3111
3112**Related docs**: Recorder installation
3113
3114---
3115
3116## Replay
3117
3118**Definition**: The execution of a recorded session in a test environment.
3119
3120**Process**:
31211. Launch browser
31222. Navigate to initial URL
31233. Replay user interactions
31244. Stub network requests
31255. Capture screenshots
3126
3127**States**:
3128- **Success**: All interactions replayed
3129- **Failure**: Errors encountered
3130- **Partial**: Some interactions skipped
3131
3132**Related concepts**: [Simulation](#simulation), [Test Run](#test-run), [Session](#session)
3133
3134**Related docs**: Architecture overview
3135
3136---
3137
3138## Replay Accuracy
3139
3140**Definition**: Percentage of user interactions successfully replayed.
3141
3142**Calculation**: (Replayed interactions / Total interactions) \xd7 100
3143
3144**Scores**:
3145- **100%**: Perfect replay
3146- **80-99%**: Mostly successful
3147- **<80%**: Significant issues
3148
3149**Factors affecting**:
3150- DOM changes (elements removed/moved)
3151- Timing issues (async loading)
3152- Non-deterministic behavior
3153
3154**Related concepts**: [Simulation](#simulation), [Replay](#replay)
3155
3156**Related docs**: Troubleshoot replay accuracy
3157
3158---
3159
3160## Screenshot
3161
3162**Definition**: An image captured during replay showing the app state at a specific moment.
3163
3164**When captured**:
3165- Page navigations
3166- Significant DOM changes
3167- User-specified moments
3168
3169**Used for**: Visual comparison between base and head test runs
3170
3171**Storage**: S3 with metadata in database
3172
3173**Related concepts**: [Diff](#diff), [Test Run](#test-run)
3174
3175**Related docs**: Architecture overview
3176
3177---
3178
3179## Secure Tunnel
3180
3181**Definition**: An encrypted connection from Meticulous' cloud environment to your CI runner, allowing tests to access locally-served apps.
3182
3183**How it works**:
31841. CI starts local app (e.g., \`localhost:3000\`)
31852. Meticulous establishes tunnel connection
31863. Cloud replay environment connects through tunnel
31874. Requests proxied to local app
3188
3189**Security**: HTTP Basic Authentication, encrypted connection
3190
3191**Debugging**: Add \`meticulous-debug\` to PR title for tunnel access
3192
3193**Related concepts**: [Cloud Compute](#cloud-compute), [Companion Assets](#companion-assets)
3194
3195**Related docs**: Tunnel advanced options
3196
3197---
3198
3199## Session
3200
3201**Definition**: A recorded user journey through your application from entry to exit.
3202
3203**Contains**:
3204- Sequence of user interactions
3205- Network requests and responses
3206- Initial URL and metadata
3207- Duration and timestamp
3208
3209**Lifecycle**:
32101. User interacts with app
32112. Recorder captures session
32123. Session uploaded to S3
32134. Session processed and stored
32145. Session may be selected for golden set
3215
3216**Example**: Homepage → Products → Add to cart → Checkout
3217
3218**Related concepts**: [Recorder](#recorder), [Replay](#replay), [Golden Set](#golden-set)
3219
3220**Related docs**: Architecture overview
3221
3222---
3223
3224## Session Recording
3225
3226**Definition**: The process of capturing user sessions using the Meticulous recorder.
3227
3228**Phase**: First phase of Meticulous workflow
3229
3230**Where it happens**: Production or staging environment with real users
3231
3232**Related concepts**: [Recorder](#recorder), [Session](#session)
3233
3234**Related docs**: Architecture overview
3235
3236---
3237
3238## Session Selection
3239
3240**Definition**: The process of choosing which recorded sessions to include in the golden set for testing.
3241
3242**Goal**: Maximize coverage while minimizing redundancy
3243
3244**Criteria**:
3245- Code coverage
3246- Page coverage
3247- Flow diversity
3248- Recency
3249
3250**When it happens**: Automatically after new sessions recorded
3251
3252**Related concepts**: [Golden Set](#golden-set), [Session](#session)
3253
3254**Related docs**: Architecture overview
3255
3256---
3257
3258## Simulation
3259
3260**Definition**: The process of replaying a session. Synonym for [Replay](#replay).
3261
3262**Simulation accuracy**: See [Replay Accuracy](#replay-accuracy)
3263
3264**Related concepts**: [Replay](#replay), [Session](#session)
3265
3266**Related docs**: Architecture overview
3267
3268---
3269
3270## Static Assets
3271
3272**Definition**: Files like JavaScript, CSS, images, fonts that don't change based on runtime logic.
3273
3274**Challenges with Meticulous**:
3275- Absolute URLs aren't automatically rewritten
3276- Large files slow down tunnel
3277- Next.js \`/_next/static/\` folders
3278
3279**Solutions**:
3280- Use relative URLs instead of absolute
3281- Use companion assets for large files
3282- Use companion assets for Next.js static folders
3283
3284**Related concepts**: [Companion Assets](#companion-assets)
3285
3286**Related docs**: GitHub Actions setup, Companion assets advanced
3287
3288---
3289
3290## Test Run
3291
3292**Definition**: A single execution of tests for a specific commit, containing multiple replays.
3293
3294**Contains**:
3295- Commit SHA
3296- Multiple replays (one per selected session)
3297- Screenshots from all replays
3298- Overall status
3299
3300**Types**:
3301- **Base test run**: On base commit
3302- **Head test run**: On head commit (PR)
3303
3304**Lifecycle**:
33051. Triggered by CI (PR or push to main)
33062. Fetch selected sessions
33073. Replay each session
33084. Capture screenshots
33095. Compare to base (if available)
33106. Post results
3311
3312**Related concepts**: [Replay](#replay), [Base Test Run](#base-test-run), [Head Test Run](#head-test-run)
3313
3314**Related docs**: Architecture overview
3315
3316---
3317
3318## Tunnel
3319
3320See [Secure Tunnel](#secure-tunnel).
3321
3322---
3323
3324## Upload Assets
3325
3326**Definition**: Meticulous execution mode where built static assets are uploaded for testing.
3327
3328**Use cases**: Static sites (Vite, Create React App) that can be served as HTML/CSS/JS
3329
3330**Not recommended for**: Next.js, server-rendered apps
3331
3332**GitHub Action**: \`alwaysmeticulous/report-diffs-action/upload-assets@v1\`
3333
3334**Alternative**: [Cloud Compute](#cloud-compute)
3335
3336**Related concepts**: [Static Assets](#static-assets)
3337
3338**Related docs**: GitHub Actions setup
3339
3340---
3341
3342## Visual Regression
3343
3344**Definition**: Unintended visual changes in the UI (layout shifts, style changes, broken components).
3345
3346**How Meticulous detects**: Screenshot comparison between base and head test runs
3347
3348**Examples**:
3349- Button moved to wrong position
3350- Text color changed unexpectedly
3351- Image not loading
3352- Layout broken on mobile
3353
3354**Related concepts**: [Diff](#diff), [Screenshot](#screenshot)
3355
3356**Related docs**: Architecture overview
3357
3358---
3359
3360## Window.Meticulous
3361
3362**Definition**: JavaScript API exposed by the Meticulous recorder for runtime integration.
3363
3364**Available methods**:
3365- \`isRunningAsTest\`: Check if running in test mode
3366- \`recordCustomValues()\`: Store custom data
3367- \`getCustomValues()\`: Retrieve stored data
3368- \`recordCustomEvent()\`: Record custom event
3369- \`pause()\`, \`resume()\`: Control replay timing
3370
3371**Availability**: Only when recorder snippet is loaded
3372
3373**Related concepts**: [Custom Values API](#custom-values-api), [Custom Event API](#custom-event-api)
3374
3375**Related docs**: window.Meticulous object reference
3376
3377---
3378
3379## Workflow
3380
3381**Definition**: CI/CD automation file that defines when and how to run Meticulous tests.
3382
3383**Common locations**:
3384- GitHub Actions: \`.github/workflows/meticulous.yaml\`
3385- GitLab CI: \`.gitlab-ci.yml\`
3386
3387**Required triggers**:
3388- \`push\` to main branch (for base runs)
3389- \`pull_request\` (for head runs)
3390- \`workflow_dispatch\` (for manual triggers)
3391
3392**Related concepts**: [Test Run](#test-run), [Base Test Run](#base-test-run)
3393
3394**Related docs**: GitHub Actions setup
3395`,es=`---
3396{
3397  "title": "Selecting Which Sessions to Run"
3398}
3399---
3400
3401# {% $frontmatter.title %}
3402
3403Meticulous replays each session that gets recorded and tracks the characters of code executed, the components rendered,
3404and the route patterns hit. Meticulous then continuously selects a combination of sessions that aim to collectively cover all of your distinct
3405characters of code (and so all feature flag branches, mutations, conditional logic branches etc.), React components and route patterns. This suite of
3406sessions is then used to test your pull requests.
3407
3408As your app changes, and as new sessions are recorded, Meticulous will automatically update the set of selected sessions to cover the new features.
3409
3410To configure the number of sessions to run, or view the current selection: visit your project page, select the 'Selected Sessions' tab, and click on 'Configure'. You generally
3411want to select sufficient sessions such that the marginal session adds no extra coverage. Meticulous provides guidance on this in the UI.
3412
3413If you're running in Meticulous cloud then test runs should generally complete in under 2 minutes.
3414
3415### Viewing your current coverage
3416
3417If you wish to view your current coverage then visit your project page and click on the 'View coverage & snapshots' button. You'll be able to
3418see the screens covered split out by route, and the sub-variants (same screen, different components visible / different states) within them.
3419If you [provide source maps](${o.ENABLE_SOURCE_COVERAGE_URL}) then you'll be able to see coverage for each folder and file in your codebase.
3420
3421### Manually selecting sessions to run
3422
3423We generally recommend to rely solely on automatic session selection, but if you wish you can select specific sessions to always be executed.
3424
3425To do so visit your project page and click on the 'Sessions' tab. Select the session you want to add and click the 'Add to selected sessions' button
3426in the top right hand corner. If the session is already added to the selected sessions, the button will say 'Remove from selected sessions'.
3427`,eo=`If you have any issues setting up the recorder then click [here](${p.METICULOUS_SETUP_CALENDLY_LINK}) to book a call with us.`,en=`
3428If you have any cross-origin or sandboxed iFrames then the recorder should be added to each of these iFrames as well as the main frame. ${eo}
3429
3430${W}
3431`,ei=`---
3432{
3433  "title": "Install the Meticulous recorder via a script tag"
3434}
3435---
3436
3437{% anchor id="${o.INSTALLATION_INSTRUCTIONS_ANCHOR}" /%}
3438# {% $frontmatter.title %}
3439
3440Please select your framework or build tool:
3441
3442{% tabs direction="grid" noTabSelectedByDefault=true %}
3443{% tab label="NextJS with the /pages directory" %}
3444## Installing on NextJS with the /pages directory
3445
3446${G({isNextJs:"yes"})}
3447
3448Add a script tag to your \`_document.js\` file within \`Head\`. If the layout doesn't yet have a \`<Head>\` tag then
3449you can add one within the \`<Html>\` tag.
3450
3451${D("Head")}
3452
3453${en}
3454{% /tab %}
3455{% tab label="NextJS with the /app directory" %}
3456## Installing on NextJS with the /app directory
3457
3458${G({isNextJs:"yes"})}
3459
3460Add a script tag to your \`/app/layout.tsx\` or \`/app/layout.jsx\` file within \`head\`. If the layout doesn't yet have a \`<head>\` tag then
3461you can add one within the \`<html>\` tag.
3462
3463${D("head")}
3464
3465After adding the snippet you'll need to follow a [few additional steps](${o.NEXTJS_APP_ROUTER_ADDITIONAL_SETUP_URL}) to ensure Meticulous can
3466correctly test your app.
3467
3468${en}
3469{% /tab %}
3470{% tab label="Nuxt" %}
3471## Installing on NuxtJS
3472
3473${G({isNextJs:"no"})}
3474
3475${j()}
3476
3477${en}
3478{% /tab %}
3479{% tab label="SvelteKit" %}
3480## Installing on SvelteKit
3481
3482${G({isNextJs:"no"})}
3483
3484${$()}
3485
3486${en}
3487{% /tab %}
3488{% tab label="Vite" %}
3489## Installing on Vite
3490
3491${G({isNextJs:"no"})}
3492
3493${q()}
3494
3495${en}
3496{% /tab %}
3497{% tab label="rsbuild" %}
3498## Installing on rsbuild
3499
3500${G({isNextJs:"no"})}
3501
3502${F()}
3503
3504${en}
3505{% /tab %}
3506{% tab label="Storybook" %}
3507## Installing on Storybook
3508
3509${G({isNextJs:"no"})}
3510
3511${H()}
3512
3513${en}
3514{% /tab %}
3515{% tab label="Any other framework or build tool" %}
3516## Installing on any other framework or build tool
3517
3518${G({isNextJs:"no"})}
3519
3520Add the recorder as the first script tag in your \`<head>
3520\` tag. If you only want to record sessions in non-production environments then
3521you will need to template your HTML to only include the script tag in non-production environments (if this is not possible then you can
3522 [use an NPM dependency instead of a script tag](${o.INSTALL_RECORDER_AS_NPM_DEPENDENCY_INSTALLATION_INSTRUCTIONS_URL})).
3523
3524{% code_with_project_selector %}
3525\`\`\`html
3526<head>
3527  ...
3528  <script
3529    data-recording-token="{% project_recording_token /%}"
3530    data-is-production-environment="<true/false>"
3531    src="${N.SNIPPET_URL}">
3532  </script>
3533
3534  <!--Meticulous snippet should be added before your app -->
3535  ...
3536  <script src="main_app.js"></script>
3537</head>
3538\`\`\`
3539{% /code_with_project_selector %}
3540
3541${en}
3542{% /tab %}
3543
3544{% /tabs %}
3545`,ea="via-cli",er="via-web",el=`---
3546{
3547  "title": "Manually Recording a Test"
3548}
3549---
3550
3551# {% $frontmatter.title %}
3552
3553Meticulous is designed to run in the background, continuously recording the hundreds of user flows you already perform naturally every day when
3554developing your application. Meticulous then automatically selects a subset of these user flows to run in CI by aiming to ensure coverage
3555over every line of code. However you can also explicitly record a specific user flow, and, if you wish, [specify it to always be run](${o.TESTING_POOL_URL}).
3556
3557There are two ways to record a test:
3558
35591. [Recording tests on an environment with the Meticulous recorder enabled](#${er})
35602. [Recording tests via the Meticulous CLI](#${ea})
3561
3562If you already have Meticulous set up, we recommend the former.
3563
3564{% anchor id="${er}" /%}
3565## Recording tests on an environment with the Meticulous recorder enabled
3566
3567Any user flows on an environment with the Meticulous recorder installed will automatically be recorded, and if they provide coverage
3568over edge cases or lines of code that other user flows do not then they will automatically be selected to run in CI. However if you wish
3569to record a specific user flow and open it in the Meticulous UI then you can do so by following the steps below:
3570
35711. Navigate to your application on an environment that you've already [configured Meticulous to record](${o.INSTALL_RECORDER_URL}), for example localhost.
35722. Perform the user flow you wish to test.
35733. Open the developer tools console: \`COMMAND + OPTION + I\` on Mac, \`CTRL + SHIFT + I\` on Windows.
35744. Run \`window.Meticulous.record.getSessionUrl()\` in the console, and click the link printed out to view the recorded session.
3575
3576{% anchor id="${ea}" /%}
3577## Recording tests via the Meticulous CLI
3578
3579You can use the Meticulous CLI to record a test from any environment:
3580
3581{% command_card title="Create tests" %}
3582
3583{% command_card_block %}
3584\`\`\`shell
3585npx @alwaysmeticulous/cli record session --apiToken="{% api_token /%}"
3586\`\`\`
3587{% /command_card_block %}
3588
3589{% /command_card %}
3590
3591* Note: the above command includes your API token. Make sure to keep this secret: it provides access to all your recorded user sessions.
3592* The command will open up a web browser with a blank page. You can now navigate to the site which you want to record a test on. This could
3593  be your production URL, or localhost.
3594* Your interactions with the site will be recorded. Go through the flow that you wish to record, like signing up.
3595* Once you are finished you can close the browser. Meticulous will print out links for the sessions recorded.
3596* If you navigate across multiple pages then Meticulous may record one separate session per page. This allows it to run the tests for the
3597  multiple pages in parallel. If it prints out multiple links then often the first ones are the login pages, and it's the last link that you
3598  want to use.
3599* Open the link to the recorded session, and click on the 'Simulate' tab. Run the command to simulate the session, and check it simulates
3600  as intended.
3601* Now that the session is recorded Meticulous will automatically test against the session in CI if Meticulous deems it to be one of the sessions
3602  that maximizes the test coverage of your application. If you want to force the session to be used then click the "Add to selected sessions"
3603  button on the session page. Learn more [here](${o.TESTING_POOL_URL}).
3604* If Meticulous isn't yet set up to run on CI then you can set it up by following the instructions [here](${o.GITHUB_ACTIONS_SETUP_URL}).
3605
3606## TypeScript Types
3607
3608For TypeScript type definitions for the \`window.Meticulous\` object, see [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}).
3609`,ec=`---
3610  {
3611    "title": "Detecting Diffs Locally"
3612  }
3613  ---
3614
3615  # {% $frontmatter.title %}
3616
3617  With the standard [Meticulous integration in your CI pipeline](https://app.meticulous.ai/docs/cloud-replay), Meticulous will automatically
3618  run on every commit to every PR and will comment on your PRs with a summary and link to the diffs.
3619
3620  However, if you want to quickly try it out before integrating with CI, you can detect diffs locally using the Meticulous CLI.
3621  This guide will walk you through the necessary steps to do so and help clarify how Meticulous works along the way.
3622
3623  ### Prerequisites
3624
3625  * **Have a Meticulous account.** If you don't have one yet, you can [sign up for free](https://app.meticulous.ai/signup).
3626  * **Install the Meticulous CLI.** The Meticulous CLI is available via [NPM](https://www.npmjs.com/package/@alwaysmeticulous/cli).
3627  * **Have a local version of your application running.** This version of your app needs to be locally accessible via a URL (e.g. http://localhost:3000).
3628
3629  ## Step 1: Record a session
3630
3631  At a high level, Meticulous works by recording user sessions and then simulating the sessions on different versions of your app to detect any changes.
3632  In order for Meticulous to detect a diff on a given screen, Meticulous needs to have recorded a session which rendered that screen.
3633
3634  If you have not yet recorded any sessions, you can use the Meticulous CLI to do so:
3635
3636  {% command_card title="Record a session" %}
3637
3638  {% command_card_block %}
3639  \`\`\`shell
3640  npx @alwaysmeticulous/cli record session --apiToken="{% api_token /%}"
3641  \`\`\`
3642  {% /command_card_block %}
3643
3644  {% /command_card %}
3645
3646  * **Note:** the above command includes your API token. Make sure to keep this secret - it provides access to all your recorded user sessions.
3647  * The command will open up a web browser with a blank page. You can now navigate to the URL for the local version of your app.
3648  * Your interactions with the site will be recorded. Go through the flow that you wish to record, like signing up.
3649  * Once you are finished you can close the browser. Meticulous will print out links for the sessions recorded.
3650  * If you navigate across multiple pages then Meticulous may record one separate session per page. This allows Meticulous to simulate sessions
3651    for multiple pages in parallel.
3652
3653  ## Step 2: Generate base screenshots
3654
3655  Meticulous does not take screenshots of your app when recording sessions. Instead, Meticulous records user actions and takes screenshots
3656  when simulating those actions against a version of your application. Comparing screenshots between simulations reduces flakes and
3657  keeps tests up-to-date as your application evolves.
3658
3659  To manually generate the base screenshots from your sessions, you can trigger a test run without providing a base test run to compare against.
3660  This can be done using the Meticulous CLI:
3661
3662  {% command_card title="Trigger a test run to generate base screenshots" %}
3663
3664  {% command_card_block %}
3665  \`\`\`shell
3666  npx @alwaysmeticulous/cli ci run-local --apiToken="{% api_token /%}" --appUrl="<LOCAL_APP_URL>"
3667  \`\`\`
3668  {% /command_card_block %}
3669
3670  {% /command_card %}
3671
3672  {% callout_card %}
3673
3674  If you have multiple sessions recorded, these test runs can take a while to complete. If you are fine with not watching the simulations in
3675  real time, you can speed up the test runs with headless mode by passing the \`--headless\` flag.
3676
3677  {% /callout_card %}
3678
3679  Once the test run completes, the CLI will output a link to the Meticulous web app where you can view the test run results. Because you did not
3680  provide a base test run to compare against, the test run page will show a message indicating that there are no diffs. If you want to see what
3681  screenshots were taken, you can click on the "View Visual Snapshots Tested" button.
3682
3683  Please note down the test run ID for use in a later step. This can be found in the URL of the test run page after \`/test-runs/\`.
3684
3685  ## Step 3: Modify your application
3686
3687  Now that you have generated base screenshots, you can modify your application to introduce a diff.
3688  Please make a change to a screen that was rendered in one of the recorded sessions and then recompile your app.
3689
3690  ## Step 4: Run the tests
3691
3692  Now that you have modified your app, you can run the tests again to see if Meticulous detects any diffs. This time, you will need to provide
3693  the test run ID from step 2 as the base test run to compare against:
3694
3695  {% command_card title="Trigger a test run to detect diffs" %}
3696
3697  {% command_card_block %}
3698  \`\`\`shell
3699  npx @alwaysmeticulous/cli ci run-local --apiToken="{% api_token /%}" --appUrl="<LOCAL_APP_URL>" --baseTestRunId="<TEST_RUN_ID>"  --parallelize
3700  \`\`\`
3701  {% /command_card_block %}
3702
3703  {% /command_card %}
3704
3705  ## Step 5: View your diffs
3706
3707  Just like in step 2, the test run in step 4 will output a link to the Meticulous web app where you can view the test run results.
3708  If Meticulous detects any diffs, you'll see both the base and the new screenshots to help you quickly identify where the diffs occurred.
3709
3710  In this demo, you recorded one or two sessions which will only cover a very small portion of your application. Once you set up [the Meticulous
3711  recorder](https://app.meticulous.ai/docs), Meticulous will auto-curate a test suite from dozens of new sessions a day and will aim to cover
3712  every corner of your application. Additionally, when you [integrate Meticulous with your CI](https://app.meticulous.ai/docs/cloud-replay),
3713  Meticulous will automatically run on every commit to every PR and will comment on your PRs with a summary and link to the diffs. These tests
3714  are run in Meticulous's simulation cluster and normally take less than 2 minutes to run.
3715
3716  ## Issues / questions?
3717
3718  We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
3719
3720  Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
3721  `,eu=`---
3722{
3723  "title": "Ensuring Base Test Runs are Available"
3724}
3725---
3726
3727# {% $frontmatter.title %}
3728
3729When running Meticulous tests in CI, Meticulous compares the test run on your
3730current commit (the "head" commit) against a test run on the base commit
3731(typically the commit on your main branch that your PR branches from).
3732If no base test run exists yet, Meticulous cannot compute visual differences.
3733
3734The \`ci prepare\` command helps ensure that a base test run
3735is available before running your tests.
3736If no base test run exists, this command will automatically trigger one using a
3737script you provide.
3738
3739## When to use this command
3740
3741You should use \`ci prepare\` if you have CI workflows where
3742base test runs might not be automatically created.
3743If you're using GitHub Actions with the standard Meticulous setup, base test
3744runs are created automatically, and you do not need this command.
3745
3746## How it works
3747
3748The \`ci prepare\` command:
3749
37501. Checks if a base test run exists for the base commit of your PR
37512. If no base test run exists, it executes your provided trigger script to create one
3752
3753You then run \`ci run-with-tunnel\` with the \`--hadPreparedForTests\` flag,
3754which signals that the command should wait for the base test run to be available
3755before proceeding.
3756
3757## Usage
3758
3759### Basic setup
3760
3761The command is meant to be used in your CI workflow before running tests:
3762
3763{% command_card title="Prepare for Meticulous tests" %}
3764
3765{% command_card_block %}
3766\`\`\`shell
3767# First, prepare and ensure base test run exists
3768npx @alwaysmeticulous/cli ci prepare \\
3769  --headCommit <commit-sha> \\
3770  --triggerScript <path-to-trigger-script>
3771
3772# Build your application
3773# (your build commands here)
3774
3775# Run Meticulous tests with the --hadPreparedForTests flag
3776npx @alwaysmeticulous/cli ci run-with-tunnel \\
3777  --commitSha <commit-sha> \\
3778  --appUrl <your-app-url> \\
3779  --hadPreparedForTests
3780\`\`\`
3781{% /command_card_block %}
3782
3783{% /command_card %}
3784
3785### Creating a trigger script
3786
3787Your trigger script should accept a commit SHA as its first argument and should:
3788
37891. Check out the commit
37902. Build the application for that commit
37913. Run Meticulous tests for that commit
3792
3793Here's an example trigger script:
3794
3795\`\`\`bash
3796#!/bin/bash
3797
3798# The commit SHA is passed as the first argument
3799BASE_COMMIT=$1
3800
3801# Clone or navigate to your repository
3802cd /tmp
3803git clone <your-repo-url>
3804cd <your-repo-name>
3805
3806# Check out the base commit
3807git checkout "$BASE_COMMIT"
3808
3809# Install dependencies, build, and start the application in background
3810npm install
3811npm run build
3812npm run preview &
3813
3814# Run Meticulous tests for this commit
3815npx @alwaysmeticulous/cli ci run-with-tunnel \\
3816  --commitSha "$BASE_COMMIT" \\
3817  --appUrl "http://localhost:8080/"
3818\`\`\`
3819
3820Make sure your trigger script is executable:
3821
3822\`\`\`bash
3823chmod +x trigger-base-test.sh
3824\`\`\`
3825
3826## Command options
3827
3828### \`ci prepare\`
3829
3830* \`--headCommit\`: The commit SHA of the head commit (the commit you're testing). Auto-detected from git if not provided.
3831* \`--triggerScript\` (required): Path to the script that should be executed to trigger a base test run
3832
3833### \`ci run-with-tunnel\` with preparation
3834
3835* \`--hadPreparedForTests\`: Signals that \`ci prepare\` was
3836  run, and the command should wait for the base test run to be available before
3837  comparing results.
3838  Note: there is a timeout for this wait, so if the base test run takes too long
3839  to complete, the command will not be stuck forever.
3840
3841* \`--triggerScript\`: Alternative approach that combines preparation and execution.
3842  Instead of using \`ci prepare\` separately followed by
3843  \`--hadPreparedForTests\`, you can pass \`--triggerScript\` directly to
3844  \`ci run-with-tunnel\`.
3845  This is more concise but may take longer to complete since it will trigger the
3846  base test run and wait for it to finish before proceeding with the current
3847  test run.
3848  Parallelizing the generation of the base test run and building the application
3849  is usually more efficient.
3850
3851## Troubleshooting
3852
3853### Base test run not being triggered
3854
3855* Verify your trigger script is executable (\`chmod +x\`)
3856* Check that the script path is correct relative to your CI working directory
3857* Ensure the script has access to necessary environment variables (like \`METICULOUS_API_TOKEN\`)
3858
3859### Tests timing out while waiting for base
3860
3861* Check that your base test run is actually completing successfully
3862* Verify the base commit SHA is correct
3863
3864### Base test run fails
3865
3866* Check the logs from your trigger script
3867* Verify the base commit can be built successfully
3868* Ensure all dependencies are available in the environment where the trigger script runs
3869
3870## More information
3871
3872For more information on setting up Meticulous in CI, see [Setting up Meticulous
3873tests to run in your CI provider](${o.GITHUB_ACTIONS_SETUP_URL}).
3874`,ed=`---
3875{
3876  "title": "window.Meticulous API Reference"
3877}
3878---
3879
3880# {% $frontmatter.title %}
3881
3882Complete reference for the \`window.Meticulous\` JavaScript API available in your application.
3883
3884---
3885
3886## Overview
3887
3888The \`window.Meticulous\` object is exposed by the Meticulous recorder snippet and provides methods for:
3889- Detecting test mode
3890- Recording custom data
3891- Controlling replay timing
3892- Handling custom events
3893
3894**Availability**: Only when Meticulous recorder is loaded.
3895
3896**TypeScript types**: See [TypeScript Types](${o.TYPESCRIPT_TYPES_URL}) for full type definitions.
3897
3898---
3899
3900## API Reference
3901
3902### isRunningAsTest
3903
3904**Type**: \`boolean | undefined\`
3905
3906**Description**: Indicates whether the app is running as a Meticulous test.
3907
3908**Values**:
3909- \`true\`: Running in test/replay mode
3910- \`false\` or \`undefined\`: Running normally (production/development)
3911
3912**Example**:
3913
3914\`\`\`typescript
3915if (window.Meticulous?.isRunningAsTest) {
3916  // Skip validation, use test data, etc.
3917  console.log('Running as Meticulous test');
3918} else {
3919  // Normal application logic
3920  console.log('Running normally');
3921}
3922\`\`\`
3923
3924**Common use cases**:
3925- Skip form validation
3926- Bypass authentication
3927- Use deterministic values (timestamps, IDs)
3928- Show a fixed app version or commit SHA instead of the build's own
3929- Disable analytics/tracking
3930- Skip animations
3931
3932---
3933
3934### recordCustomValues()
3935
3936**Signature**: \`recordCustomValues(values: Record<string, any>): void\`
3937
3938**Description**: Store custom data during recording to be retrieved during replay.
3939
3940**Parameters**:
3941- \`values\`: Object with key-value pairs to store
3942
3943**Size limits**:
3944- **Development**: 20MB total (set \`data-is-production-environment="false"\`)
3945- **Production**: 1MB total
3946
3947**When to use**:
3948- Store file upload contents
3949- Record feature flag values
3950- Save user context
3951- Preserve random/dynamic values
3952
3953**Example**:
3954
3955\`\`\`typescript
3956// During recording
3957const handleFileUpload = (file: File) => {
3958  const reader = new FileReader();
3959  reader.onload = (e) => {
3960    const fileData = e.target?.result as string;
3961
3962    // Store for replay
3963    if (window.Meticulous?.recordCustomValues) {
3964      window.Meticulous.recordCustomValues({
3965        uploadedFile: fileData,
3966        fileName: file.name,
3967        fileType: file.type,
3968      });
3969    }
3970
3971    processFile(fileData);
3972  };
3973  reader.readAsDataURL(file);
3974};
3975\`\`\`
3976
3977**Multiple calls**: Values are merged, not overwritten. Later calls add/update keys.
3978
3979\`\`\`typescript
3980// First call
3981window.Meticulous.recordCustomValues({ key1: 'value1' });
3982
3983// Second call - adds key2, keeps key1
3984window.Meticulous.recordCustomValues({ key2: 'value2' });
3985
3986// Result: { key1: 'value1', key2: 'value2' }
3987\`\`\`
3988
3989---
3990
3991### getCustomValues()
3992
3993**Signature**: \`getCustomValues(): Record<string, any> | undefined\`
3994
3995**Description**: Retrieve custom values stored during recording.
3996
3997**Returns**: Object with stored values, or \`undefined\` if none.
3998
3999**When to use**:
4000- Restore file upload data during replay
4001- Get feature flag values
4002- Restore user context
4003
4004**Example**:
4005
4006\`\`\`typescript
4007// During replay
4008useEffect(() => {
4009  if (window.Meticulous?.isRunningAsTest) {
4010    const stored = window.Meticulous.getCustomValues();
4011
4012    if (stored?.uploadedFile) {
4013      // Restore file preview
4014      setPreview(stored.uploadedFile);
4015    }
4016
4017    if (stored?.featureFlags) {
4018      // Apply feature flags
4019      setFlags(stored.featureFlags);
4020    }
4021  }
4022}, []);
4023\`\`\`
4024
4025**Complete pattern**:
4026
4027\`\`\`typescript
4028function useStoredValue(key: string) {
4029  const [value, setValue] = useState(null);
4030
4031  useEffect(() => {
4032    if (window.Meticulous?.isRunningAsTest) {
4033      const stored = window.Meticulous.getCustomValues();
4034      if (stored?.[key]) {
4035        setValue(stored[key]);
4036      }
4037    }
4038  }, [key]);
4039
4040  return value;
4041}
4042
4043// Usage
4044const userContext = useStoredValue('userContext');
4045\`\`\`
4046
4047---
4048
4049### pause()
4050
4051**Signature**: \`pause(): void\`
4052
4053**Description**: Pause replay until \`resume()\` is called.
4054
4055**When to use**:
4056- Before async operations (data loading, image loading)
4057- Before third-party script initialization
4058- When content loads dynamically
4059
4060**Important**: Always pair with \`resume()\`. Unpaired \`pause()\` will hang the test.
4061
4062**Example - Data Loading**:
4063
4064\`\`\`typescript
4065async function loadCriticalData() {
4066  window.Meticulous?.pause?.();
4067
4068  try {
4069    const data = await fetchDataFromAPI();
4070    setData(data);
4071  } finally {
4072    window.Meticulous?.resume?.();
4073  }
4074}
4075\`\`\`
4076
4077**Example - Image Loading**:
4078
4079\`\`\`typescript
4080function LazyImage({ src }: { src: string }) {
4081  const [loaded, setLoaded] = useState(false);
4082
4083  useEffect(() => {
4084    if (!loaded && window.Meticulous?.isRunningAsTest) {
4085      window.Meticulous?.pause?.();
4086    }
4087  }, [loaded]);
4088
4089  const handleLoad = () => {
4090    setLoaded(true);
4091    window.Meticulous?.resume?.();
4092  };
4093
4094  return <img src={src} onLoad={handleLoad} />;
4095}
4096\`\`\`
4097
4098---
4099
4100### resume()
4101
4102**Signature**: \`resume(): void\`
4103
4104**Description**: Resume replay after \`pause()\`.
4105
4106**When to use**: After the async operation started with \`pause()\` completes.
4107
4108**Example - Third-Party Script**:
4109
4110\`\`\`typescript
4111function loadGoogleMaps() {
4112  return new Promise((resolve) => {
4113    if (window.google?.maps) {
4114      resolve(window.google.maps);
4115      return;
4116    }
4117
4118    window.Meticulous?.pause?.();
4119
4120    const script = document.createElement('script');
4121    script.src = 'https://maps.googleapis.com/.../js?key=KEY';
4122    script.onload = () => {
4123      window.Meticulous?.resume?.();
4124      resolve(window.google.maps);
4125    };
4126    script.onerror = () => {
4127      window.Meticulous?.resume?.();
4128      reject(new Error('Failed to load'));
4129    };
4130
4131    document.head.appendChild(script);
4132  });
4133}
4134\`\`\`
4135
4136---
4137
4138### recordCustomEvent()
4139
4140**Signature**: \`recordCustomEvent(event: { type: string; payload?: any }): void\`
4141
4142**Description**: Record a custom event with optional payload.
4143
4144**Parameters**:
4145- \`event.type\`: String identifying the event type
4146- \`event.payload\`: Optional data to store with event
4147
4148**When to use**: Advanced scenarios requiring fine-grained replay control.
4149
4150**Example**:
4151
4152\`\`\`typescript
4153// Record event during session
4154function handlePaymentComplete(transaction: Transaction) {
4155  if (window.Meticulous?.recordCustomEvent) {
4156    window.Meticulous.recordCustomEvent({
4157      type: 'PAYMENT_COMPLETE',
4158      payload: {
4159        transactionId: transaction.id,
4160        amount: transaction.amount,
4161        timestamp: Date.now(),
4162      },
4163    });
4164  }
4165
4166  showSuccessMessage();
4167}
4168\`\`\`
4169
4170---
4171
4172### onReplayCustomEvent()
4173
4174**Signature**: \`onReplayCustomEvent(handler: (event: CustomEvent) => void): void\`
4175
4176**Description**: Register handler for custom events during replay.
4177
4178**Parameters**:
4179- \`handler\`: Function called when custom event is replayed
4180
4181**Example**:
4182
4183\`\`\`typescript
4184// Setup handler during component mount
4185useEffect(() => {
4186  if (window.Meticulous?.onReplayCustomEvent) {
4187    window.Meticulous.onReplayCustomEvent((event) => {
4188      if (event.type === 'PAYMENT_COMPLETE') {
4189        console.log('Replaying payment:', event.payload);
4190        handlePaymentReplay(event.payload);
4191      }
4192    });
4193  }
4194}, []);
4195\`\`\`
4196
4197---
4198
4199## Backend Detection
4200
4201### meticulous-is-test Header
4202
4203Check for the \`meticulous-is-test\` header in server-side code:
4204
4205**Value**: \`"1"\` when running as test
4206
4207**Security**: For secure detection, set a custom secret header in project settings.
4208
4209### Next.js App Router
4210
4211\`\`\`typescript
4212import { headers } from 'next/headers';
4213
4214export async function isMeticulousTest(): Promise<boolean> {
4215  const requestHeaders = await headers();
4216  return requestHeaders.get('meticulous-is-test') === '1';
4217}
4218
4219// Usage in Server Component
4220export default async function Page() {
4221  const isTest = await isMeticulousTest();
4222
4223  if (isTest) {
4224    // Skip auth, use test data, etc.
4225  }
4226
4227  return <div>...</div>;
4228}
4229\`\`\`
4230
4231### Next.js Pages Router
4232
4233\`\`\`typescript
4234export const getServerSideProps = (context) => {
4235  const { req } = context;
4236  const isTest = req.headers['meticulous-is-test'] === '1';
4237
4238  return {
4239    props: {
4240      isRunningAsMeticulousTest: isTest,
4241    },
4242  };
4243};
4244
4245function Page({ isRunningAsMeticulousTest }) {
4246  if (isRunningAsMeticulousTest) {
4247    // Test-specific rendering
4248  }
4249
4250  return <div>...</div>;
4251}
4252\`\`\`
4253
4254### Express.js
4255
4256\`\`\`typescript
4257app.get('/api/data', (req, res) => {
4258  const isTest = req.headers['meticulous-is-test'] === '1';
4259
4260  if (isTest) {
4261    // Return test data
4262    res.json({ data: 'test-data' });
4263  } else {
4264    // Return real data
4265    res.json({ data: getRealData() });
4266  }
4267});
4268\`\`\`
4269
4270---
4271
4272## Common Patterns
4273
4274### Pattern 1: Skip Validation
4275
4276\`\`\`typescript
4277function submitForm(data: FormData) {
4278  if (!window.Meticulous?.isRunningAsTest) {
4279    // Only validate in normal mode
4280    if (!data.email) {
4281      throw new Error('Email required');
4282    }
4283  }
4284
4285  // Submit form
4286  return api.post('/submit', data);
4287}
4288\`\`\`
4289
4290### Pattern 2: Deterministic Values
4291
4292\`\`\`typescript
4293function generateId(): string {
4294  if (window.Meticulous?.isRunningAsTest) {
4295    return 'test-id-12345';
4296  }
4297  return crypto.randomUUID();
4298}
4299
4300function getCurrentTimestamp(): string {
4301  if (window.Meticulous?.isRunningAsTest) {
4302    return '2024-01-01T00:00:00Z';
4303  }
4304  return new Date().toISOString();
4305}
4306\`\`\`
4307
4308### Pattern 3: Disable Third-Party Services
4309
4310\`\`\`typescript
4311function initializeServices() {
4312  if (!window.Meticulous?.isRunningAsTest) {
4313    initializeAnalytics();
4314    initializeSentry();
4315    initializeIntercom();
4316  }
4317}
4318\`\`\`
4319
4320### Pattern 4: Feature Flags
4321
4322\`\`\`typescript
4323function useFeatureFlag(flagName: string): boolean {
4324  const [enabled, setEnabled] = useState(false);
4325
4326  useEffect(() => {
4327    if (window.Meticulous?.isRunningAsTest) {
4328      const stored = window.Meticulous.getCustomValues();
4329      if (stored?.featureFlags?.[flagName] !== undefined) {
4330        setEnabled(stored.featureFlags[flagName]);
4331        return;
4332      }
4333    }
4334
4335    // Normal flag fetching
4336    fetchFlag(flagName).then(setEnabled);
4337  }, [flagName]);
4338
4339  return enabled;
4340}
4341
4342// Record flags during session
4343if (window.Meticulous?.recordCustomValues) {
4344  window.Meticulous.recordCustomValues({
4345    featureFlags: {
4346      newCheckout: true,
4347      experimentalUI: false,
4348    },
4349  });
4350}
4351\`\`\`
4352
4353### Pattern 5: Conditional Rendering
4354
4355\`\`\`typescript
4356function WelcomeBanner() {
4357  if (window.Meticulous?.isRunningAsTest) {
4358    // Don't show banner during tests
4359    return null;
4360  }
4361
4362  return <div>Welcome!</div>;
4363}
4364\`\`\`
4365
4366---
4367
4368## Integration Patterns
4369
4370### React Hook
4371
4372\`\`\`typescript
4373function useMeticulous() {
4374  return {
4375    isTest: window.Meticulous?.isRunningAsTest ?? false,
4376    recordValues: window.Meticulous?.recordCustomValues,
4377    getValues: window.Meticulous?.getCustomValues,
4378    pause: window.Meticulous?.pause,
4379    resume: window.Meticulous?.resume,
4380  };
4381}
4382
4383// Usage
4384function MyComponent() {
4385  const { isTest, recordValues } = useMeticulous();
4386
4387  if (isTest) {
4388    // Test-specific logic
4389  }
4390}
4391\`\`\`
4392
4393### Context Provider
4394
4395\`\`\`typescript
4396const MeticulousContext = createContext({
4397  isTest: false,
4398  recordValues: () => {},
4399  getValues: () => ({}),
4400});
4401
4402function MeticulousProvider({ children }) {
4403  const value = {
4404    isTest: window.Meticulous?.isRunningAsTest ?? false,
4405    recordValues: window.Meticulous?.recordCustomValues?.bind(window.Meticulous) ?? (() => {}),
4406    getValues: window.Meticulous?.getCustomValues?.bind(window.Meticulous) ?? (() => ({})),
4407  };
4408
4409  return (
4410    <MeticulousContext.Provider value={value}>
4411      {children}
4412    </MeticulousContext.Provider>
4413  );
4414}
4415
4416// Usage
4417const { isTest } = useContext(MeticulousContext);
4418\`\`\`
4419
4420---
4421
4422## Best Practices
4423
4424### 1. Always Use Optional Chaining
4425
4426\`\`\`typescript
4427// Good
4428window.Meticulous?.isRunningAsTest
4429
4430// Bad - throws error if Meticulous not loaded
4431window.Meticulous.isRunningAsTest
4432\`\`\`
4433
4434### 2. Pair pause() with resume()
4435
4436\`\`\`typescript
4437// Good - always resume
4438async function load() {
4439  window.Meticulous?.pause?.();
4440  try {
4441    await fetchData();
4442  } finally {
4443    window.Meticulous?.resume?.();
4444  }
4445}
4446
4447// Bad - might not resume
4448async function load() {
4449  window.Meticulous?.pause?.();
4450  await fetchData();
4451  window.Meticulous?.resume?.(); // Skipped if fetchData throws
4452}
4453\`\`\`
4454
4455### 3. Check isRunningAsTest Before Using Other Methods
4456
4457\`\`\`typescript
4458// Good
4459if (window.Meticulous?.isRunningAsTest) {
4460  const values = window.Meticulous.getCustomValues();
4461}
4462
4463// Also good
4464const values = window.Meticulous?.getCustomValues?.();
4465\`\`\`
4466
4467### 4. Record Early, Retrieve Early
4468
4469\`\`\`typescript
4470// Record as soon as data is available
4471useEffect(() => {
4472  if (userData && window.Meticulous?.recordCustomValues) {
4473    window.Meticulous.recordCustomValues({ user: userData });
4474  }
4475}, [userData]);
4476
4477// Retrieve during component initialization
4478useEffect(() => {
4479  if (window.Meticulous?.isRunningAsTest) {
4480    const stored = window.Meticulous.getCustomValues();
4481    if (stored?.user) {
4482      setUser(stored.user);
4483    }
4484  }
4485}, []); // Empty deps - run once
4486\`\`\`
4487
4488---
4489
4490## Troubleshooting
4491
4492### window.Meticulous is undefined
4493
4494**Cause**: Recorder not loaded
4495
4496**Solutions**:
4497- Verify recorder script tag is in HTML
4498- Check script loads before app code
4499- Check for CSP blocking
4500
4501### Custom values not available during replay
4502
4503**Cause**: Values not recorded or size limit exceeded
4504
4505**Solutions**:
4506- Check \`recordCustomValues\` was called during recording
4507- Verify value size < 1MB (prod) or 20MB (dev)
4508- Check \`data-is-production-environment\` setting
4509
4510### Pause/Resume hanging tests
4511
4512**Cause**: \`resume()\` never called
4513
4514**Solutions**:
4515- Use try/finally to ensure \`resume()\` always runs
4516- Check async callbacks complete
4517- Add timeout fallback
4518
4519---
4520
4521## TypeScript Types
4522
4523For full TypeScript type definitions:
4524
4525\`\`\`typescript
4526interface Meticulous {
4527  isRunningAsTest?: boolean;
4528  recordCustomValues?: (values: Record<string, any>) => void;
4529  getCustomValues?: () => Record<string, any> | undefined;
4530  pause?: () => void;
4531  resume?: () => void;
4532  recordCustomEvent?: (event: { type: string; payload?: any }) => void;
4533  onReplayCustomEvent?: (handler: (event: any) => void) => void;
4534}
4535
4536declare global {
4537  interface Window {
4538    Meticulous?: Meticulous;
4539  }
4540}
4541\`\`\`
4542
4543See [TypeScript Types](${o.TYPESCRIPT_TYPES_URL}) for importable types.
4544
4545---
4546
4547## See Also
4548
4549- [Record Custom Values](${o.RECORD_CUSTOM_VALUES_URL}) - Detailed custom values guide
4550- [Custom Event API](${o.USE_CUSTOM_EVENT_API_URL}) - Advanced event handling
4551- [Handle File Uploads](${o.HANDLE_FILE_UPLOADS_URL}) - File upload patterns
4552`,eh=`---
4553{
4554  "title": "Testing Multiple Apps or App Variants"
4555}
4556---
4557
4558# {% $frontmatter.title %}
4559
4560### I have a single app, but with multiple variants deployed under different configurations to different URLs. How do I use Meticulous to test them?
4561
4562When Meticulous simulates sessions it is configured to simulate the sessions against a particular base URL - for example the \`report-diffs-action\` \`appUrl\` or the URL of the Vercel deployment. This URL will
4563normally be different to the URL the session was recorded at. When simulating a session, Meticulous takes the URL the session was recorded at and swaps out the origin with the new base URL. Learn more [here](${o.BASE_URL_EXPLANATION_URL}).
4564
4565You'll therefore need to make sure that the base URL you are simulating sessions against serves up the same app under the same configuration as the base URL sessions are recorded on.
4566
4567If you have multiple URLs that serve different variants of your app, for example, customized for different customers, then you can set up multiple Meticulous projects and multiple Vercel deployments or \`report-diffs-action\` calls - one to test each variant of the app using the sessions recorded for that variant. See below for more details.
4568
4569### I have multiple independent apps in the same monorepo. How do I use Meticulous to test them?
4570
4571You'll most likely want to set up multiple Meticulous projects for the same GitHub repo. Each Meticulous project will have its own recording token, allowing you
4572to set up each app to record sessions in a different Meticulous project.
4573
4574If you're using Github Actions to run Meticulous in CI you can then set up a separate call
4575to \`report-diffs-action\` for each Meticulous project, passing in the API token of the relevant project, and pointing it to a URL that serves the correct application.
4576
4577If you're using Vercel, or another service that provides preview URLs, then you'll want to configure Vercel deployments for each application separately.
4578Navigate to the project settings page for each Meticulous project and configure that project to test against only the relevant Vercel deployments by
4579selecting the appropriate environments in the \`Environments to Test Against\` section.
4580
4581### GitHub check names with multiple projects
4582
4583When you have multiple Meticulous projects connected to the same GitHub repository, the GitHub status check name includes the project name to differentiate them. For example, instead of *'Meticulous Tests'*, the checks will be named *'Meticulous Tests (project-a)'* and *'Meticulous Tests (project-b)'*.
4584
4585If you have Meticulous configured as a [required check](/docs/make-check-blocking) in your branch protection rules, make sure the required check names match the actual check names. Adding a second project to a repo that previously only had one will change the check name, which can cause PRs to hang waiting for a check that no longer exists under the old name.
4586
4587Reach out to [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll help you get set up.
4588`,ep=`---
4589{
4590  "title": "Testing Feature Flags with Meticulous"
4591}
4592---
4593
4594# {% $frontmatter.title %}
4595
4596> **Note:** To learn how to *register* feature flags in your sessions, see [Recording the context of a user session](${o.RECORD_SESSION_CONTEXT_URL}). This page explains how Meticulous *tests* feature flags.
4597
4598By default Meticulous will snapshot the network responses, local storage values, cookies and session storage values when a session is
4599originally recorded, and [stub out and replay those values](${o.NETWORK_STUBBING_EXPLANATION_URL}) when later replaying the session. This allows
4600Meticulous to test across varied feature flag configurations.
4601
4602If two recorded sessions executed different code because a flag was on in one and off in the other, Meticulous's
4603[coverage-based test suite](${o.TESTING_POOL_URL}) will often keep both. Replays stub the recorded network and storage, so those sessions keep
4604the flag values they were recorded with. Meticulous does not enumerate flag combinations as its own selection signal.
4605
4606For example, say user 1 records session A with *my_new_feature* off, and user 2 records session B with *my_new_feature* on. When Meticulous
4607replays session A it will replay with *my_new_feature* disabled, and when it replays session B it will replay with *my_new_feature* enabled.
4608If those sessions cover different code paths, both are likely to stay in the suite.
4609
4610### Letting Meticulous Override Named Feature Flags
4611
4612Meticulous can ask your application to use a specific value for a *named* feature flag during a replay,
4613so that a replay can exercise code behind a flag that was off — or did not exist — when the session was
4614recorded.
4615
4616To opt in, resolve the value once: prefer \`getFlagOverride\`, otherwise use your own evaluation, then
4617**record that same value** with \`recordFeatureFlag\` before returning it. The two APIs are complementary
4618— \`getFlagOverride\` asks Meticulous which value to use, and \`recordFeatureFlag\` tells Meticulous which
4619value your app actually used. Recording a snapshot from \`getAllFlags()\` (or similar) will miss a forced
4620value, because the SDK never saw the override.
4621
4622\`\`\`typescript
4623const checkGate = (gateName: string) => {
4624  const override = window.Meticulous?.context?.getFlagOverride?.(gateName);
4625  const value = override?.overridden
4626    ? Boolean(override.value)
4627    : myStatsigClient.checkGate(gateName);
4628  window.Meticulous?.context?.recordFeatureFlag?.(gateName, value);
4629  return value;
4630}
4631\`\`\`
4632
4633The same few lines work for any on/off gate, including one you've written yourself. For example,
4634if your app resolves flags from the query string:
4635
4636\`\`\`typescript
4637const resolveFlag = (flagKey: string) => {
4638  const override = window.Meticulous?.context?.getFlagOverride?.(flagKey);
4639  const value = override?.overridden
4640    ? Boolean(override.value)
4641    : flagsFromQueryString[flagKey] || false;
4642  window.Meticulous?.context?.recordFeatureFlag?.(flagKey, value);
4643  return value;
4644}
4645\`\`\`
4646
4647How you consume \`override.value\` depends on the helper you wrap — \`Boolean()\` is not always right.
4648Always record the flag's resolved value (the thing that describes the cohort), not a comparison boolean:
4649
4650- **On/off gate** (\`checkGate(name)\` returns a boolean): \`Boolean(override.value)\`, as above. Record that boolean.
4651- **Value-read** (\`getExperimentValue(name)\` / \`variation(name)\` returns the cohort): return and record \`override.value\` as-is. \`Boolean('control')\` is \`true\`, which would turn every variant on.
4652- **Equality-check** (\`isTreatment(name, expected)\` / \`editorExperiment(name, expected)\` returns a boolean meaning "is this flag set to \`expected\`"): compare, don't coerce. Record \`override.value\`, not the \`===\` result. Wrapping with \`Boolean(override.value)\` makes every \`expected\` match; returning the string is equally wrong because callers expect a boolean:
4653
4654\`\`\`typescript
4655const isTreatment = (name: string, expected: string | boolean) => {
4656  const override = window.Meticulous?.context?.getFlagOverride?.(name);
4657  if (override?.overridden) {
4658    window.Meticulous?.context?.recordFeatureFlag?.(name, override.value);
4659    return override.value === expected;
4660  }
4661  return originalIsTreatment(name, expected);
4662}
4663\`\`\`
4664
4665If you wrap the value-read underneath an equality-check (\`getExperimentValue\` under \`editorExperiment\`),
4666use the value-read rule and record there — the \`===\` already happens above you.
4667
4668Hook the helper your application actually calls. \`getFlagOverride\` returns \`{ overridden: false }\` whenever
4669Meticulous has no override for that flag, and always does so for real users being recorded, so it is safe
4670to leave in production code. Use optional chaining as shown above so your application also works when the
4671Meticulous snippet isn't loaded.
4672
4673It also composes with the blanket default described below: check for an override first, then fall back to
4674defaulting unrecognised flags to enabled, then record the value you return.
4675
4676### Improving Test Coverage with New Feature Flags
4677
4678As mentioned above, sessions recorded with different flag values will keep those values on replay, and
4679coverage-based selection will often keep both. Sessions recorded *prior* to a feature flag being introduced
4680will likely not test the new feature, because they replay old saved network responses and local storage
4681values without an entry for it. Test coverage of new features gated behind flags can therefore stay limited
4682until new sessions are recorded with that flag enabled — unless you opt into \`getFlagOverride\` above, or
4683the default-enabled fallback below.
4684
4685You can enable unrecognised flags by default when running as part of a Meticulous test (i.e. when Meticulous
4686is replaying old network responses or local storage values from before the flag was introduced). We've
4687included instructions for Statsig below, but a similar approach can be applied for any feature flagging
4688framework. **We recommend making this change if you often develop new features gated behind feature flags.**
4689
4690### Configuring Meticulous with Statsig
4691
4692Before checking a feature flag value first check [window.Meticulous?.isRunningAsTest](${o.METICULOUS_WINDOW_OBJECT_URL}), and if so then
4693check if configuration for the feature flag is missing entirely - if it is then default the feature flag to enabled. This then allows
4694Meticulous to use old sessions to test new features. Here's an example for Statsig, however similar approaches can be followed for other
4695feature flagging frameworks, and for Statsig's React integration:
4696
4697\`\`\`typescript
4698import { StatsigClient } from '@statsig/js-client';
4699
4700const myStatsigClient = new StatsigClient(
4701  YOUR_CLIENT_KEY,
4702  { userID: 'a-user' },
4703  ...
4704);
4705await myStatsigClient.initializeAsync();
4706
4707// We wrap checkGate, and use the wrapped version in our code instead of directly calling myStatsigClient.checkGate
4708const checkGate = (gateName: string) => {
4709  const override = window.Meticulous?.context?.getFlagOverride?.(gateName);
4710  if (override?.overridden) {
4711    const value = Boolean(override.value);
4712    window.Meticulous?.context?.recordFeatureFlag?.(gateName, value);
4713    return value;
4714  }
4715
4716  // If the application is running as part of a Meticulous test, and Meticulous is replaying old network responses or local storage values
4717  // from before the feature gate was introduced then let's default the feature to on, so that Meticulous can use old user sessions to test
4718  // new features.
4719  //
4720  // See https://docs.statsig.com/sdk/debugging
4721  const value =
4722    window.Meticulous?.isRunningAsTest
4723      && myStatsigClient.getFeatureGate(gateName).details.reason.endsWith("Unrecognized")
4724      ? true
4725      : myStatsigClient.checkGate(gateName);
4726  window.Meticulous?.context?.recordFeatureFlag?.(gateName, value);
4727  return value;
4728}
4729\`\`\`
4730
4731### Configuring Meticulous with LaunchDarkly
4732
4733When accessing a variation default it to true if [window.Meticulous?.isRunningAsTest](${o.METICULOUS_WINDOW_OBJECT_URL}) is true, after
4734checking for a named override. Record the value you return:
4735
4736\`\`\`typescript
4737const variation = (flagKey: string, defaultValue: boolean) => {
4738  const override = window.Meticulous?.context?.getFlagOverride?.(flagKey);
4739  const value = override?.overridden
4740    ? override.value
4741    : client.variation(
4742        flagKey,
4743        window.Meticulous?.isRunningAsTest ?? defaultValue,
4744      );
4745  window.Meticulous?.context?.recordFeatureFlag?.(flagKey, value);
4746  return value;
4747}
4748\`\`\`
4749
4750We recommend wrapping your client to make this the automatic behavior rather than updating every call site.
4751
4752## Related Pages
4753
4754- [Recording the context of a user session](${o.RECORD_SESSION_CONTEXT_URL}) - Learn how to record feature flags and other session context
4755- [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}) - Get type definitions for the \`window.Meticulous\` object
4756`,em=`---
4757{
4758  "title": "Troubleshoot issues when recording on one environment and simulating on another"
4759}
4760---
4761
4762# {% $frontmatter.title %}
4763
4764You can record sessions from one environment (for example, production, or localhost) and simulate them against another environment
4765(for example, a preview URL). However the sessions may fail to simulate if there are significant differences between the environments, for example:
4766
4767 - **Static assets with absolute URLs**: Meticulous automatically swaps the base URL (origin) for page navigation and API requests made via fetch/XHR. However, **static assets (CSS, JavaScript, images) that are referenced with absolute URLs in your HTML are NOT automatically rewritten**.
4768
4769   For example, if your HTML contains \`<script src="https://production.example.com/dist/app.js"></script>\`, this URL will NOT be rewritten when simulating against \`http://localhost:3000\`. The browser will still try to load assets from the original absolute URL.
4770
4771   **Solution**: Use relative URLs for static assets in your HTML:
4772   \`\`\`html
4773   <!-- Instead of this: -->
4774   <script src="https://production.example.com/dist/app.js"></script>
4775
4776   <!-- Use this: -->
4777   <script src="/dist/app.js"></script>
4778   \`\`\`
4779
4780   This ensures assets are loaded from whatever environment the session is being simulated against.
4781
4782 - **Differences in authentication configuration**: If the authentication is configured differently on the different environments, and the frontend javascript performs s
4782ome basic authentication checks before talking to the backend then sessions may not simulate correctly across environments.
4783
4784   Since Meticulous mocks out any requests to the backend, and doesn't hit your real backend, it doesn't matter if the environment hits a different backend or a different auth service. However if, for example, the FE checks for the existence of a certain local storage value to check auth, and redirects the user to the login page if it's absent, and the name of the local storage value that is checked is different between the two environments, then sessions recorded on one environment may not simulate correctly on the other.
4785
4786 - **Fundamental differences in the URL routing, rather than just the base URL**: Meticulous can handle differences in the [base URL](${o.BASE_URL_EXPLANATION_URL})/the origin, however it may not work if there are more fundamental differences in the URL routes between the different environments (for example, one environment uses a path parameter where another uses a query parameter). Click [here](${o.BASE_URL_EXPLANATION_URL}) for more details.
4787
4788If you're using Vercel, Netlify, or similar preview URLs, then Meticulous will simulate sessions against the preview URL of the base commit
4789on the main branch and against the preview URL of the head commit of the pull request branch, and compare visual snapshots between the two. It's therefore
4790also important that the preview URLs of the main branch and the pull request branches are configured to run the app with the same configuration.
4791In this case please check the environment variables and configuration you use to run & build your app are the same for the deployments of the
4792main branch (production deploys) and the deployments of pull request branches (preview deploys). If the configuration is not aligned then it's
4793possible that you could get [false positive screenshot diffs](${o.FIX_FALSE_POSITIVES_URL}).
4794
4795### How do I solve this?
4796
4797To fix these issues you may need to unify some of the configurations across the environments you are recording and simulating on. For example,
4798if there are fundamental differences in the URL routing, then you could alias the routes on the different environments so that they match,
4799and sessions can be simulated.
4800
4801You can also use the [\`Meticulous\` object on the window](${o.METICULOUS_WINDOW_OBJECT_URL}) to detect if the page is being rendered as part of a Meticulous
4802test, rather than for an end user, and if so modify the behaviour of the app to work around the differences between the environments. For
4803example, if sessions are failing to simulate because the frontend code looks for a different cookie name on the different environment to check if the user is authenticated,
4804then you could disable the frontend-only authentication checks if the page is being rendered as part of a Meticulous test. Since the user won't be authenticated any requests
4805to the backend would fail, but since Meticulous mocks out all responses from the backend anyway it doesn't matter.
4806
4807If you have any questions reach out to [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll be happy to help.
4808`,ef="server-side-rendering",eg="env-differences",ey="build-specific-values",ew="pausing-meticulous-replays",eb="other-causes",ev="general-techniques",ek=`---
4809{
4810  "title": "Fix False Positive Diffs"
4811}
4812---
4813
4814# {% $frontmatter.title %}
4815
4816## Overview
4817
4818Ideally every difference you see in the Meticulous UI for a given PR should be directly caused by a code change introduced by that PR. Differences that
4819show up that are unrelated to your code change could be due to:
4820
4821 - [Changes in content derived from database data, or the current date/time, when using server side rendering](#${ef}) (when using client side rendering Meticulous handles this automatically)
4822 - Or, [differences in how you build the code for the two different environments you're comparing between (e.g. PR build vs main branch build)](#${eg})
4823 - Or, [version numbers, commit SHAs or build timestamps baked into the build](#${ey})
4824 - Or, [executing asynchronous tasks that Meticulous doesn't natively handle](#${ew})
4825 - Or, [other causes](#${eb})
4826
4827In all of these cases, if you can't solve the underlying cause, then you can just mark the diff to be ignored:
4828
4829{% anchor id="${ev}" /%}
4830### Configuring certain diffs to be ignored
4831
4832You can configure diffs inside certain elements to be ignored by adding a CSS selector to the
4833_'Elements to ignore when comparing screenshots'_ list in the _'Screenshotting behavior'_ section in project settings, or by adding the
4834\`meticulous-ignore\` class to an element.
4835
4836Please note that if you open a pull request to add a \`meticulous-ignore\` class to an element then the ignore rule will only apply to Meticulous
4837test runs for new PRs opened since the original PR adding the \`meticulous-ignore\` class was merged.
4838
4839Alternatively, you can detect if the app is being rendered as part of a Meticulous test, and disable part of the UI or code that is causing
4840false positive diffs when being rendered in a test. For frontend components you can use the [Meticulous object on the window](${o.METICULOUS_WINDOW_OBJECT_URL}),
4841and for server side components or server side rendering you can use the [\`meticulous-is-test\` header](#${ef}).
4842
4843{% anchor id="${ef}" /%}
4844## Diffs due to changes in data or the current date when using server side rendering, or rendering NextJS server components
4845
4846By default Meticulous stubs out responses for any fetch or XHR requests from the browser and stubs out the Date functions in the browser.
4847This means that even if the data in your database changes or the time changes you won't see any false positive diffs.
4848
4849However if you're using NextJS server components then Meticulous will re-render those server components on the backend every time it replays
4850a session -- this means that if the data in your database changes or the date changes in the short window of time between when Meticulous
4851replays the session on the base commit and when Meticulous replays the session on the head commit, and you render that data or the date to
4852the page inside a server component, then you could see false positive diffs.
4853
4854Meticulous sends a \`meticulous-is-test\` header in every request it makes to your NextJS server. You can use this header to disable parts of
4855your server components which cause flakes in Meticulous tests. It'll always be present (with value '1') if the request is being made as part
4856of a Meticulous test.
4857
4858If you render text based on the current time (for example "Posted 7 minutes ago"), you can configure Meticulous to send a simulated date
4859header with the virtual time by adding a custom header in your project settings (Sett
4859ings > Custom Request Headers). Use the
4860**Simulated Date** template to set the header value - this will be resolved per-request to the virtual time in RFC 7231 format.
4861
4862For example, you could add a custom header named \`meticulous-simulated-date\` using the Simulated Date template, then use it like so:
4863
4864\`\`\`javascript
4865import { headers } from 'next/headers'
4866
4867const getCurrentDate = async () => {
4868  const requestHeaders = await headers()
4869
4870  // If a simulated date header is configured in Meticulous project settings, use it instead of the current date
4871  const simulatedDate = requestHeaders.get('meticulous-simulated-date')
4872  return simulatedDate ? new Date(Date.parse(simulatedDate)) : new Date()
4873}
4874\`\`\`
4875
4876This avoids false positive diffs due to the time changing (e.g. "Posted 7 minutes ago" vs "Posted 8 minutes ago"): Meticulous will send
4877the same timestamp every time for the same request. The timestamp is a UTC date in RFC 7231 format.
4878
4879You can also [configure Meticulous to ignore the diffs using CSS selectors](#${ev}).
4880
4881{% anchor id="${eg}" /%}
4882## Diffs due to differences between environments
4883
4884### Using Vercel
4885
4886If you use Vercel then Meticulous will try to automatically generate previews using the same environmental configuration for both commits to the main branch,
4887and to PR branches. So you shouldn't see any false positive diffs due to differences between environments. If you do, then reach out to
4888the [Meticulous support team](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
4889
4890### Using Netlify, or other preview providers
4891
4892If you use another preview URL provider, such as Netlify, then Meticulous will compare visual snapshots from the preview URL of the base commit on the main branch to snapshots from the preview URL of the head commit of the pull request branch.
4893
4894In this case the environment variables and configuration you use to run & build your app needs to be the same for the deployments of the main branch (production deploys) and the deployments of pull request branches (preview deploys). If this isn't the case Meticulous could display false screenshot differences.
4895
4896For example if you configure production deploys of your app (from the main branch) to have a blue banner, and preview deploys of your app (from pull request branches)
4897 to have a red banner, then Meticulous would display screenshot diffs of the banner changing from blue to red for every screen. You want to make
4898 sure that the only screenshot diffs Meticulous shows are due to changes in the code introduced by the pull request being tested, rather than
4899 environmental differences between the environments tested on.
4900
4901To fix this check the environment variables and configuration you use to run & build your app are the same for the deployments of the
4902main branch (production deploys) and the deployments of pull request branches (preview deploys).
4903
4904If it's not possible to unify the configuration across the environments then you can [configure Meticulous to ignore the diffs](#${ev}).
4905
4906### Using GitHub Actions
4907
4908If, instead of preview URLs, you're using the \`report-diffs-action\` GitHub action, then Meticulous will compare snapshots from running your app from the base
4909commit of the main branch to snapshots from running your app from the head commit of the pull request branch. In this case it's similarly important to make sure
4910that you compile and run your app with the same configuration for both the main branch and the pull request branches.
4911
4912{% anchor id="${ey}" /%}
4913## Diffs due to version numbers, commit SHAs or build timestamps
4914
4915Meticulous compares a replay against the base commit's build with a replay against the head commit's build, so any value baked in at
4916build time -- an app version, a git tag or commit SHA, a build date -- is different on every pull request. This causes false positive diffs in two ways:
4917
4918 - **The value is displayed**, for example "v2.14.0 \xb7 3f9c2ab" in a footer, sidebar or about dialog. Every screenshot showing it has a diff.
4919 - **The value is compared against a response**, for example an update banner ("A new version is available") that compares the bundled version with a
4920   polled \`/api/version\` or \`version.json\`. Meticulous replays the response captured when the session was recorded (often from a local dev
4921   server, or an older deployment), so the versions almost never match and the banner covers the page -- or the app reloads itself mid-replay.
4922
4923To fix this, make the app use a fixed value whenever it runs as a Meticulous test.
4924
4925For an update check, skip the check itself -- pinning the bundled version doesn't help, because the recorded response still carries whatever
4926version the recording environment reported:
4927
4928\`\`\`javascript
4929const isUpdateAvailable = (deployedVersion) => {
4930  // Meticulous replays the recorded /api/version response, which won't match this build's version
4931  if (window.Meticulous?.isRunningAsTest) {
4932    return false
4933  }
4934  return Boolean(deployedVersion) && deployedVersion !== APP_VERSION
4935}
4936\`\`\`
4937
4938For a displayed value, show a placeholder in the same format:
4939
4940\`\`\`javascript
4941const getDisplayVersion = () =>
4942  window.Meticulous?.isRunningAsTest ? '0.0.0-meticulous' : APP_VERSION
4943\`\`\`
4944
4945If the value is also rendered on the server (server side rendering or NextJS server components), return the same placeholder on the server
4946too, otherwise the server HTML still differs and hydration can fail. If you build a separate copy of your app for Meticulous in CI, you can pin the
4947value in your build config (e.g. webpack \`DefinePlugin\` or Vite \`define\`) for that build. Otherwise, check for the
4948[\`meticulous-is-test\` header](#${ef}) on the server.
4949
4950Only change values that are visible on the page -- values sent as error-reporting release tags or analytics properties can keep their real value.
4951
4952{% anchor id="${ew}" /%}
4953## Diffs due to asynchronous tasks not handled natively by Meticulous
4954
4955Meticulous will automatically wait for most browser tasks to complete before continuing with javascript execution. This ensures the
4956resultant screenshots are deterministic. However, if your application waits for asynchronous events that are *not* handled natively by Meticulous
4957you can use the [Meticulous object on the window](${o.METICULOUS_WINDOW_OBJECT_URL}) to pause the execution of the replay while the
4958asynchronous task is in-progress.
4959
4960For example, let's say you send a message to a custom Chrome extension and then wait for a response.
4961In this case you can tell Meticulous to pause the replay until you have received the expected response:
4962
4963\`\`\`javascript
4964function sendMessageToExtension() {
4965  if (window.Meticulous?.isRunningAsTest) {
4966    // Meticulous will pause test execution for up to 30 seconds. If we don't
4967    // call pause() here Meticulous will sometimes take a screenshot before the
4968    // Chrome extension has responded, and sometimes after, causing flaky tests.
4969    window.Meticulous.replay.pause();
4970  }
4971  chrome.runtime.sendMessage(MY_EXTENSION_ID, "My message", (response) => {
4972    if (window.Meticulous?.isRunningAsTest) {
4973      // Important: we continue the replay even if the request fails
4974      window.Meticulous.replay.resume();
4975    }
4976    if (response.success) {
4977      doSomething(response.data);
4978    }
4979  });
4980}
4981\`\`\`
4982
4983{% anchor id="${eb}" /%}
4984## False positive diffs due to other reasons
4985
4986Meticulous ensures the session simulation executes identically every time, even if there are animations, timers, random number generators,
4987changing data, or changing dates and times. So under normal operation false positive diffs or flakes should not happen.
4988
4989However, if you are making extensive use of web workers, WebGL or WASM, it is possible that in some cases you could see false
4990positive diffs. If you do notice a false positive diff please reach out to the [Meticulous support team](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and
4991we'll take a look into it. You can also [configure Meticulous to ignore the diffs](#${ev}).
4992
4993${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
4994`,eS=`---
4995{
4996  "title": "Recording custom values to use when replaying user sessions"
4997}
4998---
4999
5000# {% $frontmatter.title %}
5001
5002In some situations, you may want to record custom values to use when replaying user sessions. This is typically not needed, but can be
5003useful in some cases — for instance, if:
5004- Meticulous is being served static content when running tests, but your server would normally inject some dynamic values into the page.
5005- You've overridden the default behaviour and configured Meticulous to log in as a single user, but you need to record some user-specific values to replay the session as a different user.
5006
5007### How can I record values?
5008
5009During recording of a user session, you can record custom values using the methods on the \`window.Meticulous.record\` object:
5010- **For object values:** Call \`recordCustomData(key, value)\`. If the value is already present,
5011it will be overwritten.
5012- **For array values:** Call \`pushToCustomDataArray(arrayId, valueToAppend)\`. If the array is not already present, it will be created.
5013The new value will be appended to the array.
5014
5015Note that both these methods take only strings as keys or values, so if you need to record a complex object you'll need to serialize
5016it to a string. For instance you could do:
5017\`\`\`javascript
5018window.Meticulous?.record?.recordCustomData(
5019  "preRenderedData",
5020  JSON.stringify(window.PRE_RENDERED_DATA)
5021);
5022\`\`\`
5023For further information, consult [the code](https://github.com/alwaysmeticulous/meticulous-sdk/blob/617538aae2c00f17155af50d1daf9208353b3956/packages/sdk-bundles-api/src/window-api/public-window-api.ts#L46-L60).
5024
5025### How can I use the recorded values?
5026
5027When Meticulous is running tests, you can access the recorded values in your code by using the methods on the \`window.Meticulous.replay\`
5028object:
5029- **For object values:** Call \`retrieveCustomData(key)\`. This will return \`null\` if the key is not found.
5030- **For array values:** Call \`retrieveCustomDataArray(arrayId)\`. This will return an empty array if the array is not found.
5031
5032For instance, you could do:
5033\`\`\`javascript
5034const preRenderedData = window.Meticulous?.replay?.retrieveCustomData("preRenderedData");
5035if (preRenderedData) {
5036  window.PRE_RENDERED_DATA = JSON.parse(preRenderedData);
5037}
5038\`\`\`
5039
5040For object values, you can also inject these into request headers in the \`Custom Request Headers\` section of the project settings.
5041
5042## TypeScript Types
5043
5044For TypeScript type definitions for the \`window.Meticulous\` object, see [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}).
5045
5046${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5047`,eT=`---
5048{
5049  "title": "Recording the context of a user session"
5050}
5051---
5052
5053# {% $frontmatter.title %}
5054
5055When recording user sessions with Meticulous, you can add contextual information. This can
5056make sessions easier to find, help your developers debug diffs in these sessions more
5057easily, and let Meticulous optimise session selection across different combinations of
5058context (e.g. flags, roles, themes).
5059
5060All \`window.Meticulous?.context.*\` calls below use optional chaining, so they're safe
5061no-ops when the recorder isn't loaded — there's no need to guard them or check
5062\`isRunningAsTest\`.
5063
5064If your project uses TypeScript, install
5065[\`@alwaysmeticulous/sdk-bundles-api\`](${o.TYPESCRIPT_TYPES_URL}) and augment the \`Window\`
5066interface as shown in the [TypeScript Types page](${o.TYPESCRIPT_TYPES_URL}); that gives
5067every call below full type safety. You only need to do this once per project — the same
5068augmentation covers \`recordUserId\`, \`recordUserEmail\`, \`recordFeatureFlag\`,
5069\`getFlagOverride\`, \`recordCustomContext\`, and the rest of \`window.Meticulous\`.
5070
5071## Adding context to user sessions
5072
5073Meticulous provides several methods to record different types of context. If you record
5074the same piece of context multiple times, the last value will be used.
5075
5076### Recording user information
5077
5078You can record the ID and email address of the logged-in user:
5079
5080\`\`\`js
5081// Record the ID of the logged-in user
5082window.Meticulous?.context.recordUserId('user-123');
5083
5084// Record the email address of the logged-in user
5085window.Meticulous?.context.recordUserEmail('[email protected]');
5086\`\`\`
5087
5088This information is associated with the session and makes it easier to find sessions for
5089specific users.
5090
5091A natural place for these calls is wherever you load the current user (e.g. a
5092\`useCurrentUser\` / \`useSession\` hook, or a \`/me\` query). If your app has a separate
5093post-login flow where the user starts logged out and then signs in, you may also want to
5094record there so those sessions pick up the user identity too.
5095
5096### Recording feature flags
5097
5098You can record which feature flags were active during a session (the value should be a
5099string or boolean):
5100
5101\`\`\`js
5102window.Meticulous?.context.recordFeatureFlag('bigUiRefactor', true);
5103window.Meticulous?.context.recordFeatureFlag('checkoutFlowStyle', 'v3');
5104\`\`\`
5105
5106Record the value your app **actually used** after any override. The usual place is the
5107same helper that resolves the flag:
5108
5109\`\`\`js
5110const resolveFlag = (flagKey) => {
5111  const override = window.Meticulous?.context?.getFlagOverride?.(flagKey);
5112  const value = override?.overridden
5113    ? Boolean(override.value)
5114    : flagsFromYourApp[flagKey] || false;
5115  window.Meticulous?.context?.recordFeatureFlag?.(flagKey, value);
5116  return value;
5117};
5118\`\`\`
5119
5120That keeps recording and overriding on one path. \`recordFeatureFlag\` only stores a value;
5121it does not change what a replay sees. \`getFlagOverride\` is what lets Meticulous force a
5122flag so a replay can exercise code that was off when the session was recorded. How you
5123consume \`override.value\` depends on whether the helper is an on/off gate, a value-read,
5124or an equality-check — see
5125[Testing Feature Flags with Meticulous](${o.TESTING_FEATURE_FLAGS}).
5126
5127If you only want to record (and are not wrapping a resolver), you can still loop over the
5128flags your app already evaluates:
5129
5130\`\`\`js
5131// Use whichever flag map your app already has — an SDK snapshot
5132// (e.g. client.getAllFlags() / posthog.getAllFlags()), an API
5133// response from your backend, or a shared flag-provider value.
5134const flags = client.getAllFlags();
5135for (const [name, value] of Object.entries(flags)) {
5136  window.Meticulous?.context.recordFeatureFlag(name, value);
5137}
5138\`\`\`
5139
5140Do **not** rely on that snapshot if you also call \`getFlagOverride\` in the resolver: the
5141SDK will still report the recorded / stubbed value, not the forced one. Record inside the
5142resolver instead.
5143
5144If your app uses **both** a client-side SDK *and* server-evaluated flags (whose resolved
5145values reach the frontend via something like a \`features\` field on \`/me\`), it's worth
5146looping over both — they each affect what the UI renders. Recording the same flag twice
5147is fine; the last value wins.
5148
5149A reasonable place to call this is wherever flags first become available (the SDK's
5150initial-fetch callback, or the effect that resolves your flags API response). If your app
5151re-evaluates flags after login or identity changes, recording there as well keeps the
5152context accurate for sessions that started logged out.
5153
5154### Recording custom context
5155
5156For any other contextual information that doesn't fit into the categories above, you can
5157use the custom context method (again with a string or boolean value):
5158
5159\`\`\`js
5160window.Meticulous?.context.recordCustomContext('userRole', 'admin');
5161\`\`\`
5162
5163Anything that changes how the UI looks or behaves between sessions is worth considering,
5164since recording it helps Meticulous tell those differences apart from real diffs. Common
5165examples:
5166
5167- **User role / permissions** — admin vs regular user, role-gated menus and actions.
5168  Often available on the same user object you read for \`recordUserId\`.
5169- **Tenant / organization / workspace ID** — for multi-tenant apps, which tenant is
5170  active.
5171- **Theme / colour scheme** — whatever drives the \`dark\` class on \`<html>\` or your
5172  theme provider.
5173- **Locale / language** — i18n setting from cookies, localStorage, \`useLocale\`,
5174  \`next-intl\`, \`i18next\`, etc.
5175- **Viewport / layout mode** — compact vs comfortable, sidebar collapsed, etc., when
5176  saved per-user.
5177- **Plan / subscription tier** — free vs pro vs enterprise, when it changes the UI.
5178- **Environment or build version** — useful when comparing diffs across deploys.
5179- **A/B test assignments** outside your main flag provider.
5180
5181It's usually enough to record each value once, where it's first read or initialised. If
5182the value can change mid-session (a theme toggle, a locale switcher, a tenant switcher),
5183recording in the change handler too keeps the context accurate.
5184
5185## Related Pages
5186
5187- [Testing Feature Flags with Meticulous](${o.TESTING_FEATURE_FLAGS}) - Learn how Meticulous tests the feature flags you record
5188- [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}) - Get type definitions for the \`window.Meticulous\` object
5189
5190${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5191`,e_=`---
5192{
5193  "title": "Troubleshoot Authentication & Authorisation Issues"
5194}
5195---
5196
5197# {% $frontmatter.title %}
5198
5199Auth issues are some of the most common issues that you might encounter while setting up Meticulous.
5200This class of issues is easily identifiable by sessions recorded on logged-in pages which, when simulated, consistently redirect to
5201log-in pages or 401 screens.
5202
5203By default Meticulous automatically stubs all XHR, Fetch and WebSocket requests to your backend, and therefore does not necessarily need to
5204authenticate to your backend. In addition Meticulous will also automatically record and replay cookies,
5205local storage & session storage, and so will often be able to authenticate automatically.
5206
5207However if you use server side rendering (SSR),
5208React server components, or wish to test your backend then you may need Meticulous to be able
5209to authenticate correctly with your backend. This works out of the box if your cookie expiries are long enough (at least a week) and
5210you're not using http only cookies. If this isn't the case click [here](${o.AUTH_ENABLING_FULL_AUTH_URL}) for docs on how to enable
5211Meticulous to authenticate correctly with your backend.
5212
5213However even if you are only using Meticulous to test your frontend, there are still some common issues that can arise:
5214
5215### 1. Backend auth checks when serving the document's HTML
5216
5217When requesting your app's document/HTML your backend may check the user's authentication and redirect them to a login page if they are no
5218longer authenticated. If this is the case you'll either need to [disable this redirect](${o.AUTH_BYPASSING_AUTH_URL}) when replaying the Meticulous tests against CI/preview URLs, or allow
5219[Meticulous to authenticate correctly against your backend](${o.AUTH_ENABLING_FULL_AUTH_URL}).
5220
5221### 2. Different auth setups across environments
5222
5223This can be an issue if all of the following hold:
5224
5225  - The environment you replay sessions against in CI and the environment you record sessions on (e.g. localhost) use different auth setups, and:
5226  - Your _frontend_ code checks the user's authentication status, and:
5227  - This check would fail if the cookies or local storage were from an environment with a different auth setup (for example, your FE code
5228    redirects the user to login unless a cookie with name \`auth.\${environment-name}\` exists).
5229
5230In this case you'll either need to [disable the FE check when running Meticulous tests](${o.AUTH_BYPASSING_AUTH_URL}), or standardize your auth provider client across environments.
5231
5232### 3. You are using Auth0
5233
5234In this case you may need to configure Auth0 to store user session data in local storage, or not use httpOnly cookies. See [here](${o.AUTH_ENABLING_FULL_AUTH_URL}?tab=Auth0) for more information.
5235
5236## Issues / questions?
5237
5238We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
5239
5240Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
5241
5242`,eI=`---
5243{
5244  "title": "Troubleshoot Recorder"
5245}
5246---
5247
5248# {% $frontmatter.title %}
5249
5250## Validating installation
5251
5252Once you add the Meticulous snippet, open your webapp (either locally or on the environment that you injected the snippet into) and record a session by clicking around on your web app.
5253
5254If the snippet was installed successfully you should be able to view the recorded session in your
5255{% project_link %}Meticulous dashboard {% /project_link %} in the **Sessions** section.
5256
5257
5258## I see a warning about high abandon rate
5259
5260If you see a warning about high abandon rate, it means that the Meticulous recorder is abandoning for a large number of sessions due to high load on the network.
5261The Meticulous recorder snippet will automatically abandon if it detects that the load on the network is too large.
5262
5263This is a protection mechanism for production deployments to prevent any performance degradation.
5264
5265It is unnecessary for staging and internal deployments. You can add \`data-is-production-environment="false"\` attribute to your script tag which will increase the threshold at which the snippet abandons.
5266Or alternatively, if using the \`@alwaysmeticulous/recorder-loader\` package you can set \`isProduction: false\` when calling \`tryLoadAndStartRecorder\`.
5267
5268Setting the 'isProduction' flag does not affect whether the recorder will activate in production environments. Instead, it only marks recordings as having been made in a production environment and uses settings that are more appropriate for production recording. If you wish to disable recording in production, see instructions [here](${o.INSTALL_RECORDER_URL}) on how to set up the recorder only on specific environments.
5269
5270
5271## I've installed the snippet but why do I not see any sessions in my Meticulous dashboard?
5272
5273It is possible that the Meticulous recorder snippet is abandoning due to high load on the network.
5274
5275You can verify whether this is the issue or not by adding a query param \`?meticulousForceRecording=true\` to the URL and then verifying whether sessions appear in your dashboard.
5276If they do, then the snippet is abandoning due to large payloads being sent to Meticulous. See the section above for how to fix this.
5277
5278
5279## Session recordings are still abandoned even after I've added \`data-is-production-environment="false"\` to the script tag
5280
5281Setting \`data-is-production-environment="false"\` will increase the threshold at which the snippet abandons, but it will not prevent it from abandoning completely.
5282
5283If you wish to fully disable this behaviour, you can add \`data-force-recording="true"\` to your script tag. This will force the snippet to record all sessions regardless of the load on the network.
5284Alternatively, if using the \`@alwaysmeticulous/recorder-loader\` package you can set \`forceRecording: true\` when calling \`tryLoadAndStartRecorder\`.
5285
5286
5287## Issues / questions?
5288
5289We're always happy to help you with any issues you encounter while setting up or anything you might be unsure about.
5290
5291Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
5292
5293## Next Steps
5294
5295* Relax and sit down while Meticulous is collecting session data for you
5296* [Set up tests to run in CI](${o.CI_SETUP_URL})
5297`,eR=`---
5298  {
5299    "title": "Troubleshoot Simulation Accuracy"
5300  }
5301  ---
5302
5303  # {% $frontmatter.title %}
5304
5305  ## I see inaccurate simulation differences on my PR test run
5306
5307  If you see inaccurate simulation differences on your PR test run, it means that the simulation against the new app version could not
5308  reproduce critical user events and page navigations that occurred during the simulation against the old app version. Inaccurate simulations
5309  can be caused by:
5310  1. **A change in your application since the session was recorded.** Changes such as moving a navigational button or modifying the network
5311  API can cause the session to replay incorrectly against new versions of your app. If your pull request made this type of change, then this
5312  is likely the cause, and you can safely ignore the inaccurate simulation diffs. If you want to check directly whether the inaccuracies are
5313  due to a change, you can look for any network requests or “click” user events that were successful in the replay timeline before your
5314  change and unsuccessful in the replay timeline after your change.
5315  2. **A difference between the environment the session was recorded on and the environment it is being replayed in**. You can test if this
5316  is the case by replaying the session against the environment it was originally recorded on and the environment it is being replayed in -
5317  see [Troubleshooting failed simulations](${o.TROUBLESHOOTING_FAILED_SIMULATIONS_STEPS_URL}) for more detailed instructions and for advice on how to fix this.
5318  3. **Something else.** You can debug what may be causing it by replaying the session locally, or looking at the replay timeline. In this
5319  case reach out to Meticulous support by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), and we'll help to get
5320  Meticulous working for your application.
5321
5322  ## I see a warning about low simulation accuracy on my project dashboard
5323
5324  If you see a warning about low simulation accuracy, it means that less than 40% of recent simulations can reproduce critical user events
5325  and navigate to all pages within the first 5 minutes of a recorded user session. The primary impact of this issue is reduced test c
5325overage
5326  for your app until the simulation accuracy improves. See [Troubleshooting failed simulations](${o.TROUBLESHOOTING_FAILED_SIMULATIONS_URL})
5327  for more detailed instructions and for advice on how to fix this.
5328
5329  ## Issues / questions?
5330
5331  We're always happy to help you with any issues you encounter while setting up or anything you might be unsure about.
5332
5333  Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), or [book a call with us](${p.METICULOUS_SETUP_CALENDLY_LINK}),
5334  and we'll help you get set up.
5335  `,eC=`---
5336  {
5337    "title": "Troubleshoot Failed or Inaccurate Simulations"
5338  }
5339  ---
5340
5341  # {% $frontmatter.title %}
5342
5343  ## Some simulations on my PR test run are failing or inaccurate
5344
5345  Simulations can fail or replay inaccurately for a few reasons:
5346
5347  1. **The session is out of date**: Your application has changed significantly since the session was originally recorded, such that the session can't be replayed against
5348  the new version of your application. This could be due to UI changes, such that the session can't interact with the same elements. Or
5349  it could be due to your application's API changing, triggering errors when Meticulous replays old out-of-date network requests from the
5350  original session. Such failing replays can be safely ignored: Meticulous will automatically detect if the session is no longer able to cover certain code paths or user
5351  flows on your new codebase and select one or more new sessions, with new network responses and user interactions, to correctly cover these paths.
5352  2. **There is an error with your recorder setup**, for example [it's not being initialized early enough so not capturing all network responses](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL}).
5353  3. **There are fundamental differences between the environment the session was recorded on and the environment it is being replayed in**, [such
5354  that sessions recorded on one environment can't be replayed on another](${o.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}).
5355
5356  Follow the steps below to distinguish between these causes and troubleshoot why simulations may be failing.
5357
5358  {% anchor id="${o.TROUBLESHOOTING_FAILED_SIMULATIONS_STEPS_ANCHOR}" /%}
5359  ## Troubleshooting why simulations may be failing
5360
5361  ### 1. Check if the user session can be simulated accurately *against the environment from which it was recorded*
5362
5363  To verify whether the user session can be simulated accurately in the same environment in which it was recorded, you can:
5364
5365  1. Find a session from the **All Sessions** or **Selected Sessions** tab of your {% project_link %}Meticulous dashboard{% /project_link %}.
5366  2. Follow the command to "Replay this session" on the **Simulate** tab of the session page.
5367  3. Note that if the session was originally recorded on localhost then the \`--appUrl\` flag will be set to a localhost URL, and you'll
5368  need to serve your app on that same port locally to replay the session (or edit the \`--appUrl\`).
5369
5370  While simulating the session, keep an eye out for common symptoms of an unsuccessfully simulated session:
5371  - The simulation hits an error screen
5372  - The simulation fails to populate pages with data
5373  - The simulation hits an error state while navigating through a form and stays there for the duration of the simulation
5374
5375  If you see any of these symptoms, and the application has changed somewhat since the session was first recorded, then the
5376  session is likely failing to replay because it is out of date, and there has since been a breaking API schema change or a breaking UI change.
5377  Such replay failures can be safely ignored: Meticulous will automatically detect if the session is no longer able to cover certain code paths or user
5378  flows on your new codebase and select one or more new sessions, with new network responses and user interactions, to correctly cover these paths. For
5379  more information on how Meticulous selects sessions, see [here](${o.TESTING_POOL_URL}).
5380
5381  If the simulation was successful, proceed to the next troubleshooting step.
5382
5383  ### 2. Check if the user session can be simulated accurately *against the environment used for test runs*
5384
5385  To verify whether the user session can be simulated accurately in the environment used for test runs, you can:
5386
5387  1. Navigate back to the session you selected in the previous step.
5388  2. Click on the **Simulations** tab to find a recent simulation in CI. You'll be able to tell it's a CI simulation since the 'Simulated Against'
5389  URL will show a Meticulous secure tunnel URL (\`*.tunnels.meticulous.ai\`) or a Vercel, Netlify or Cloudflare preview URL.
5390  3. Select one of the simulations and follow the command to "Re-run the simulation with the Meticulous CLI" on the **${i.SIMULATION_TAB_NAMES.DEBUG_LOCALLY}** tab of the simulation page.
5391
5392  If you're triggering Meticulous tests in CI against Vercel, Netlify or Cloudflare preview URLs then you can run this command as is:
5393  the \`--appUrl\` it is set to run against will most likely still be a valid preview URL.
5394
5395  If however you're triggering Meticulous tests in
5396  CI against a Meticulous secure tunnel URL or a localhost server running in your CI runner then you'll need to edit the \`--appUrl\` to point to a valid URL. If you're
5397  using the cloud replay action you can get a URL to run against by opening a new PR with \`${i.METICULOUS_DEBUG_PR_LABEL}\` in the title. This
5398  will keep the secure tunnel open for debugging. Meticulous will post a comment on your PR on how to access the secure tunnel to debug
5399  your application setup, and you can use this as the \`--appUrl\` to replay the simulation against. It will also post a username and password
5400  which you must make available as \`METICULOUS_TUNNEL_USERNAME\` and \`METICULOUS_TUNNEL_PASSWORD\` environment variables when running the Meticulous
5401  CLI.
5402
5403  Look out for any of the symptoms described in step 1. If the simulation fails when simulating against the test run environment but not when simulating against the original recording
5404  environment then the issue is likely environmental differences between the recording and test run environments. For more
5405  information on how to debug environmental differences, see [here](${o.FIX_FALSE_POSITIVES_URL}).
5406
5407  If the simulation was successful, please get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), or
5408  [book a call with us](${p.METICULOUS_SETUP_CALENDLY_LINK}), and we will help you debug this issue.
5409
5410  ### 3. View the simulation timeline to see why it failed to replay
5411
5412  Find the simulation in the Meticulous web UI and click on the **${i.SIMULATION_TAB_NAMES.TIMELINE_AND_LOGS}** tab. From here you can zoom in using the scroll wheel and
5413  pan by dragging. You'll be able to see each screenshot taken, each click, each console log recorded, and each network request made. Failures
5414  are highlighted in red.
5415
5416  Alternatively, simulating a session locally via the **${i.SIMULATION_TAB_NAMES.DEBUG_LOCALLY}** tab with the flags \`--debugger --devTools\`
5417  will let you step through the simulation one user event at a time and pinpoint exactly where the issue is occurring.
5418  `,ex=`---
5419{
5420  "title": "Ensuring the recorder captures early network requests"
5421}
5422---
5423
5424# {% $frontmatter.title %}
5425
5426The Meticulous recorder captures all fetch, XHR and web socket network requests and responses. These same responses are then used when Meticulous later
5427simulates the session to test your code. To do this Meticulous needs to be the first script to execute so that it can override and wrap
5428\`window.fetch\` and \`XMLHttpRequest\` before any other scripts execute, since these scripts may snapshot references to the original version
5429of the objects.
5430
5431{% anchor id="how-to-make-recorder-first-script" /%}
5432### How do I ensure the Meticulous recorder script is the first script to execute?
5433
5434Please see the instructions [here](${o.INSTALL_RECORDER_URL}) for installing the recorder on your particular framework.
5435
5436**If you're loading the recorder via an NPM dependency:**
5437
5438If you're using
5439\`@alwaysmeticulous/recorder-loader\` then you'll need to wait for the promise returned by \`tryLoadAndStartRecorder\` to
5440resolve before you initialise your app, trigger any network requests, or load any libraries that might take a reference to \`window.fetch\` or
5441\`XMLHttpRequest\`.
5442
5443If you are already waiting for the promise returned by \`tryLoadAndStartRecorder\` to resolve before making any network
5444requests then it may be the case that a library you depend upon is snapshotting a reference to the native version of \`window.fetch\`
5445or \`XMLHttpRequest\` (rather than the version with the Meticulous interceptors) at import time before the recorder script initializes, and then later using
5446this to make network requests. If this is the case then you can switch to [installing the recorder as a script tag](${o.INSTALL_RECORDER_AS_SCRIPT_TAG_URL}) instead.
5447
5448**If you're loading the recorder via a script tag:**
5449
5450If you're adding the Meticulous recorder as a script tag, then you'll need to make sure:
5451
54521. That it is added to your \`index.html\` file, *before* any other script tags.
54532. That it does not have any async or defer attributes set, and, if using NextJS, that it uses the native \`script\` tag instead of the NextJS \`Script\` component.
54543. That it is present in the initial HTML returned from the server -- you cannot add the script tag dynamically using JavaScript, since if
5455you do so the browser may execute the script after other scripts have loaded. If you need to include the script tag in your HTML only
5456in certain environments then this must be done either server-side, or at build time by templating your HTML.
5457
5458${B}
5459
5460{% anchor id="auto-detect-installation-problems" /%}
5461### How can I detect if I installed it correctly?
5462
5463The best way is to look at the network tab of the browser in the environment where you've added the recorder snippet. You should see a GET
5464request to ${N.SNIPPET_URL} prior to any other network activity (as a note, the Meticulous snippet makes a
5465call to Sentry when it initializes, so you might see that early on in the network tab as well).
5466
5467Meticulous can also sometimes automatically detect if the script is not installed correctly. The script monitors for \`window.performance\` entries to
5468detect network requests that occurred before the recorder script initialized. If it detects any missed network requests when recording a session
5469a warning will be shown on the page for that session. However, this only detects cases where the network requests occur before the
5470recorder script initializes, and doesn't detect cases where another script or library which initializes before the recorder script stores a
5471reference to the native \`window.fetch\` or \`XMLHttpRequest\` and then _later_ uses that stored reference to make a network request,
5472without Meticulous being able to intercept it.
5473
5474### Why does Meticulous record and replay network requests?
5475
5476Recording and replaying network requests and responses has a couple of key advantages:
5477
5478  (1) Meticulous does not need to hit your backend when running the tests, so there's no risk of a test causing side effects. This means
5479  you don't need to set up separate test user accounts.
5480
5481  (2) Meticulous can replay the same responses every time, so that the tests are deterministic and don't depend on the state of your
5482  backend. This allows Meticulous to safely compare the results of the test runs from before your code change and after your code change,
5483  while ensuring that the only differences spotted come from your change to the code: your tests are automatically idempotent. As your BE
5484  APIs change Meticulous will automatically swap out old tests for new ones, keeping them up to date.
5485
5486${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5487`,eA=`---
5488{
5489  "title": "Viewing source coverage information in Meticulous"
5490}
5491---
5492
5493# {% $frontmatter.title %}
5494
5495Meticulous can be set up to show you what parts of your code are being tested by us. In order to use this feature you must be serving [source maps](https://web.dev/articles/source-maps) for your code in at least some of the environments you're testing against (for instance, if building source maps significantly slows down your build you could do this only on pushes
5496to your main branch to avoid delaying PR runs).
5497
5498## How can I view my source coverage?
5499
5500To view your coverage information, visit your project's landing page on Meticulous (that is, the \`Overview\` tab). From there click the
5501\`View coverage & snapshots\` button, and select the \`Sources\` tab. You'll see a list of all the files in your project, and for each file
5502you'll see a percentage of the lines in that file that are covered by your tests. You can click on a file to see the exact lines that are
5503covered and not covered.
5504
5505## How can I serve source maps so Meticulous finds them?
5506
5507Meticulous will autodetect your source maps in three different ways (you only need to do one of these):
5508
55091. You serve the source map in the same directory as the file it corresponds to, with the same name as the file, but with a \`.map\`
5510extension added at the end. For instance, if you have a file \`https://mysite.com/static/assets/index.js\` then you should serve the
5511source map at \`https://mysite.com/static/assets/index.js.map\`.
55122. You have a \`sourceMappingURL\` comment at the end of your file that points to the source map as documented
5513[here](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html).
55143. You set the \`SourceMap\` HTTP header on the response that serves the file to point to the source map as documented
5515[here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap).
5516
5517
5518## CSS source maps
5519
5520Meticulous also reports which of your stylesheets each test covered. Doing that needs a source map for the bundled CSS your app
5521loaded, which is a separate artifact from your JavaScript source maps — turning source maps on for JavaScript does not
5522necessarily produce one. Meticulous finds a CSS source map in the same three ways listed above: a \`.map\` file served next to the
5523CSS asset, a \`sourceMappingURL\` comment at the end of the stylesheet, or a \`SourceMap\` response header.
5524
5525### Vite
5526
5527Vite does not emit CSS source maps for production builds at all
5528([vitejs/vite#2830](https://github.com/vitejs/vite/issues/2830)), and \`build.sourcemap: true\` covers JavaScript only — it does
5529nothing for CSS. Without a CSS source map a bundled stylesheet is opaque to us, and its coverage cannot be attributed to any file
5530in your repository.
5531
5532Our plugin closes that gap. It reconstructs a \`<asset>.css.map\` for every CSS asset your build emits, and appends the
5533\`sourceMappingURL\` comment that points at it:
5534
5535\`\`\`shell
5536npm install @alwaysmeticulous/recorder-plugin@latest --save-dev
5537\`\`\`
5538
5539\`\`\`typescript
5540// vite.config.ts
5541import CssSourcemapPlugin from "@alwaysmeticulous/recorder-plugin/css-sourcemap";
5542import { defineConfig } from "vite";
5543
5544export default defineConfig({
5545  plugins: [CssSourcemapPlugin()],
5546});
5547\`\`\`
5548
5549The same plugin is available as the named export \`cssSourcemapPlugin\` if you prefer. It is independent of the recorder-injection
5550plugin in the same package, so you can use either or both, and it works under Rolldown — used by both the \`rolldown-vite\`
5551package and Vite 8 — as well as under stock Vite.
5552
5553If your Vite project lives in a subdirectory rather than at the root of your repository, pass \`root\` so that the emitted source
5554paths match the paths Meticulous sees:
5555
5556\`\`\`typescript
5557CssSourcemapPlugin({ root: path.resolve(__dirname, "../..") });
5558\`\`\`
5559
5560#### How precise is the attribution?
5561
5562Every stylesheet is attributed to the file it came from. Line-level accuracy depends on the stylesheet:
5563
55641. Plain CSS in its own file maps line for line.
55652. Sass/SCSS and Less map approximately — Vite drops preprocessor maps in production.
55663. Tailwind \`@import\`s map to the imported file and line for rules the plugin can still find in the compiled CSS. Generated
5567utilities stay on the Tailwind entry, not on the \`className\` that produced them.
55684. A non-Tailwind \`@import\` that Vite inlines still gets the right file, but line numbers below the import 
5568can shift.
5569
5570File-level attribution is enough to see which stylesheet a change touched. Tailwind imported rules also get line-level maps;
5571utilities do not.
5572
5573#### The tradeoff: CSS minification
5574
5575The plugin disables Vite's CSS minification, and that is what makes the emitted maps accurate — Vite minifies a CSS asset after
5576the plugin has recorded where each stylesheet landed inside it. Measured on two real apps, the shipped CSS grew by about 11%
5577gzipped on a React and Mantine app, and about 6% on a Tailwind app, with build time within noise. The \`.css.map\` files themselves
5578are only fetched by tooling and never on a page load, so they add nothing to what your users download.
5579
5580Because of that cost, enable the plugin on the build whose coverage Meticulous collects rather than on every production build.
5581You can keep minification on with \`disableCssMinify: false\`, but then the plugin emits no map at all and warns, rather than
5582emitting a partial one that would attribute some stylesheets' rules to their neighbours.
5583
5584
5585## Monorepos
5586
5587If you&apos;re using a monorepo, you&apos;ll need to build source maps for each package in your monorepo that your app depends on.
5588
5589For example, if your app is within \`apps/frontend\` and you have a package \`packages/utils\` that the app depends on,
5590you&apos;ll need to build source maps for both of these packages. You might need to adjust your build process to load source maps
5591for your dependencies, e.g. by using \`source-map-loader\` in your webpack config.
5592
5593
5594### Example Next.js setup
5595
55961. Enable source maps generation in your dependent packages.
5597  \`packages/utils/tsconfig.json\`:
5598    \`\`\`json
5599    {
5600      ...
5601      "sourceMap": true,
5602      "declarationMap": true
5603    }
5604    \`\`\`
56052. Install \`source-map-loader\` in your Next.js project:
5606      \`\`\`bash
5607      npm install --save-dev source-map-loader <OR>
5608      yarn add --dev source-map-loader <OR>
5609      pnpm add --save-dev source-map-loader
5610      \`\`\`
56113. Add the following to your Next.js project&apos;s \`next.config.js\` in order to load source maps from your monorepo packages:
5612      \`\`\`javascript
5613      module.exports = {
5614        ...
5615        webpack: (config, { isServer }) => {
5616          // Ensure TypeScript source maps from monorepo packages work correctly
5617          config.module.rules.push({
5618            test: /\\.js$/,
5619            use: ['source-map-loader'],
5620            enforce: 'pre',
5621          });
5622
5623          // Silence source map parsing warnings.
5624          config.ignoreWarnings = [
5625            ...(config.ignoreWarnings || []),
5626            /Failed to parse source map/,
5627          ];
5628
5629          return config;
5630        },
5631      };
5632      \`\`\`
5633
5634${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5635`,eM=`---
5636{
5637  "title": "Blocking Requests During Replay"
5638}
5639---
5640
5641# {% $frontmatter.title %}
5642
5643Meticulous automatically stubs network requests during replay to ensure deterministic test results. However, some third-party scripts and services may still cause non-determinism by executing outside of the normal request flow (e.g. via service workers, WebSockets, or inline script tags).
5644
5645You can configure **blocked request patterns** to abort these requests during replay, preventing them from interfering with your tests.
5646
5647## When to use blocked requests
5648
5649Use blocked requests when a third-party service causes flaky diffs or non-deterministic behavior during replay. Common examples include:
5650
5651- **Analytics and tracking scripts** (e.g. Segment, Amplitude, Google Analytics)
5652- **Error monitoring** (e.g. Sentry, Datadog, LogRocket)
5653- **Payment processors** (e.g. Stripe)
5654- **CAPTCHAs** (e.g. reCAPTCHA, hCaptcha)
5655- **Chat widgets** (e.g. Intercom, Zendesk)
5656- **Ad scripts**
5657
5658## Configuring blocked requests
5659
56601. Navigate to your project's **Settings** page.
56612. Scroll to the **Blocked Requests** section.
56623. Click **Add Entry** to add a new pattern.
56634. Fill in one or more of the following fields:
5664   - **Root Domain**: Match requests to a specific domain (e.g. \`stripe.com\`). This matches the domain and all of its subdomains.
5665   - **Resource Type**: Filter by the type of resource being requested (e.g. Script, Document, Image).
5666   - **URL Regex**: A regular expression to match against the full request URL. Use this for more granular control.
56675. Click **Save Changes**.
5668
5669You can combine multiple fields in a single entry to create more specific rules. For example, setting both a root domain and a resource type will only block requests that match both conditions.
5670
5671## How matching works
5672
5673A request is blocked if it matches **any** of the entries in your blocked requests list. Within a single entry, **all** specified fields must match:
5674
5675- **Root Domain**: The request URL's hostname must be the domain itself or a subdomain of it. For example, \`stripe.com\` matches \`js.stripe.com\` and \`api.stripe.com\`.
5676- **Resource Type**: The request's resource type must exactly match (e.g. \`script\`, \`fetch\`, \`xhr\`).
5677- **URL Regex**: The regular expression must match somewhere in the full request URL. The regex is tested against the complete URL string.
5678
5679
5680${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5681`,eE=`---
5682{
5683  "title": "Configuring Ignore Patterns for Coverage"
5684}
5685---
5686
5687# {% $frontmatter.title %}
5688
5689Meticulous uses a \`.meticulousignore\` file at the root of your repository to exclude files from [source coverage](${o.ENABLE_SOURCE_COVERAGE_
5689URL}) tracking. The file follows the same syntax as [.gitignore](https://git-scm.com/docs/gitignore).
5690
5691## Global ignore patterns
5692
5693Create a \`.meticulousignore\` file at the root of your repository. Any file whose path matches a pattern in this file is excluded from coverage across all Meticulous projects that point to the repository.
5694
5695\`\`\`
5696# .meticulousignore
5697
5698# Exclude generated files
5699src/generated/**
5700
5701# Exclude Storybook
5702apps/storybook/**
5703
5704# Exclude mobile-specific files
5705**/*.ios.*
5706**/*.android.*
5707\`\`\`
5708
5709## Project-specific ignore patterns (monorepos)
5710
5711If your repository contains multiple Meticulous projects — for example, separate \`dashboard\` and \`admin\` apps in a monorepo — you can create per-project ignore files so that each project only tracks coverage for its own source files.
5712
5713Create a \`.meticulousignore.{slug}\` file at the root of your repository, where \`{slug}\` is derived from your Meticulous project name (see [How the slug is computed](#how-the-slug-is-computed) below).
5714
5715For example, a monorepo with a \`dashboard\` project and an \`admin\` project might have:
5716
5717\`\`\`
5718# .meticulousignore.dashboard
5719# Applied only when running coverage for the "dashboard" project
5720
5721apps/admin/**
5722\`\`\`
5723
5724\`\`\`
5725# .meticulousignore.admin
5726# Applied only when running coverage for the "admin" project
5727
5728apps/dashboard/**
5729\`\`\`
5730
5731Patterns from \`.meticulousignore\` (global) and \`.meticulousignore.{slug}\` (project-specific) are merged together, so both apply at the same time.
5732
5733## How the slug is computed
5734
5735The slug is derived from your Meticulous project name using these steps:
5736
57371. Convert to lowercase
57382. Replace any character that is not alphanumeric, a hyphen (\`-\`), or an underscore (\`_\`) with a hyphen
57393. Collapse consecutive hyphens into a single hyphen
57404. Strip any leading or trailing hyphens
5741
5742| Project name | Ignore file |
5743|---|---|
5744| \`dashboard\` | \`.meticulousignore.dashboard\` |
5745| \`my-next-cloudflare-app\` | \`.meticulousignore.my-next-cloudflare-app\` |
5746| \`meticulous_app\` | \`.meticulousignore.meticulous_app\` |
5747| \`My Dashboard App\` | \`.meticulousignore.my-dashboard-app\` |
5748| \`apps/admin\` | \`.meticulousignore.apps-admin\` |
5749| \`My App (v2)\` | \`.meticulousignore.my-app-v2\` |
5750
5751Your project name is shown in the Meticulous UI in the top-left of your project's page, and in the URL: \`app.meticulous.ai/projects/{org}/{project-name}\`.
5752
5753${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5754`,eU=`---
5755{
5756  "title": "Handle file uploads"
5757}
5758---
5759
5760# {% $frontmatter.title %}
5761
5762## Overview
5763
5764By default, Meticulous does not automatically store files that users upload during recording. This design decision is intentional:
5765
5766- **Storage efficiency**: Recording file contents would significantly increase storage costs
5767- **Privacy**: Avoiding file storage prevents capturing potentially sensitive user data
5768- **Performance**: File uploads can be large and would slow down session recording
5769- **Practicality**: Most apps only need to test the upload flow, not the file contents themselves
5770
5771When a user uploads a file during a recorded session (via HTML file input or drag-and-drop), Meticulous records the user interaction but not the file itself. During replay, the file won't be attached to the input element.
5772
5773This guide shows you how to handle file uploads in your Meticulous tests using three different approaches.
5774
5775---
5776
5777## Approach 1: Skip Validation (Recommended)
5778
5779**When to use**: Most cases where your app validates that a file is present before proceeding.
5780
5781The recommended approach is to bypass file validation during test runs. Since Meticulous stubs out network requests, your backend will respond with mocked data as if the file was successfully uploaded, allowing you to test the complete user flow.
5782
5783### Basic Example
5784
5785\`\`\`typescript
5786const handleClickUpload = () => {
5787  if (window.Meticulous?.isRunningAsTest) {
5788    // Skip validation during Meticulous tests
5789    goToNextStage();
5790  } else if (fileInput.files.length > 0) {
5791    goToNextStage();
5792  } else {
5793    showIsRequiredError();
5794  }
5795}
5796\`\`\`
5797
5798### Single File Input with Validation
5799
5800\`\`\`typescript
5801function ProfilePictureUpload() {
5802  const [file, setFile] = useState<File | null>(null);
5803  const [error, setError] = useState<string>('');
5804
5805  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
5806    const selectedFile = event.target.files?.[0];
5807    if (selectedFile) {
5808      setFile(selectedFile);
5809      setError('');
5810    }
5811  };
5812
5813  const handleSubmit = async () => {
5814    // Skip file validation during Meticulous tests
5815    if (!window.Meticulous?.isRunningAsTest && !file) {
5816      setError('Please select a file');
5817      return;
5818    }
5819
5820    // During tests, formData will be empty, but the mocked backend response
5821    // will return as if the file was successfully uploaded
5822    const formData = new FormData();
5823    if (file) {
5824      formData.append('profilePicture', file);
5825    }
5826
5827    const response = await fetch('/api/upload-profile-picture', {
5828      method: 'POST',
5829      body: formData,
5830    });
5831
5832    const data = await response.json();
5833    // data.imageUrl will be the mocked value during tests
5834    showSuccessMessage(\`Uploaded: \${data.imageUrl}\`);
5835  };
5836
5837  return (
5838    <div>
5839      <input type="file" onChange={handleFileChange} accept="image/*" />
5840      {error && <span className="error">{error}</span>}
5841      <button onClick={handleSubmit}>Upload</button>
5842    </div>
5843  );
5844}
5845\`\`\`
5846
5847### Multiple File Inputs
5848
5849\`\`\`typescript
5850function DocumentUploadForm() {
5851  const [resume, setResume] = useState<File | null>(null);
5852  const [coverLetter, setCoverLetter] = useState<File | null>(null);
5853
5854  const handleSubmit = async () => {
5855    // Validate files only when not running as a test
5856    if (!window.Meticulous?.isRunningAsTest) {
5857      if (!resume) {
5858        alert('Resume is required');
5859        return;
5860      }
5861      if (!coverLetter) {
5862        alert('Cover letter is required');
5863        return;
5864      }
5865    }
5866
5867    const formData = new FormData();
5868    if (resume) formData.append('resume', resume);
5869    if (coverLetter) formData.append('coverLetter', coverLetter);
5870
5871    await fetch('/api/submit-application', {
5872      method: 'POST',
5873      body: formData,
5874    });
5875
5876    // Backend response is mocked during tests, so this will work
5877    navigateToConfirmationPage();
5878  };
5879
5880  return (
5881    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
5882      <div>
5883        <label>Resume (Required)</label>
5884        <input
5885          type="file"
5886          onChange={(e) => setResume(e.target.files?.[0] || null)}
5887          accept=".pdf,.doc,.docx"
5888        />
5889      </div>
5890      <div>
5891        <label>Cover Letter (Required)</label>
5892        <input
5893          type="file"
5894          onChange={(e) => setCoverLetter(e.target.files?.[0] || null)}
5895          accept=".pdf,.doc,.docx"
5896        />
5897      </div>
5898      <button type="submit">Submit Application</button>
5899    </form>
5900  );
5901}
5902\`\`\`
5903
5904### Drag-and-Drop Upload
5905
5906\`\`\`typescript
5907function DragDropUpload() {
5908  const [file, setFile] = useState<File | null>(null);
5909  const [isDragging, setIsDragging] = useState(false);
5910
5911  const handleDrop = (e: React.DragEvent) => {
5912    e.preventDefault();
5913    setIsDragging(false);
5914
5915    const droppedFile = e.dataTransfer.files[0];
5916    if (droppedFile) {
5917      setFile(droppedFile);
5918    }
5919  };
5920
5921  const handleUpload = async () => {
5922    // Skip validation during tests
5923    if (!window.Meticulous?.isRunningAsTest && !file) {
5924      alert('Please drop a file first');
5925      return;
5926    }
5927
5928    const formData = new FormData();
5929    if (file) {
5930      formData.append('file', file);
5931    }
5932
5933    await fetch('/api/upload', { method: 'POST', body: formData });
5934    showSuccessMessage();
5935  };
5936
5937  return (
5938    <div
5939      onDrop={handleDrop}
5940      onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
5941      onDragLeave={() => setIsDragging(false)}
5942      className={isDragging ? 'dragging' : ''}
5943    >
5944      {file ? \`Selected: \${file.name}\` : 'Drop file here'}
5945      <button onClick={handleUpload}>Upload</button>
5946    </div>
5947  );
5948}
5949\`\`\`
5950
5951### Why This Works
5952
5953Since Meticulous stubs out network responses from your backend, requests like \`POST /api/upload\` or \`GET /api/file/123\` will replay with the exact responses that were captured during recording. This means:
5954
59551. During recording: Real file uploaded → Real backend response captured → Response includes file metadata/URL
59562. During replay: No file uploaded → Mocked backend response → Same response as recording, so app behaves identically
5957
5958You get complete test coverage of the user flow without needing the actual file contents.
5959
5960---
5961
5962## Approach 2: Store File Contents (For Frontend Processing)
5963
5964**When to use**: Your app needs a saved value at a known point during replay, and does not need to reproduce the timing of each file selection.
5965
5966Use the [custom values API](${o.RECORD_CUSTOM_VALUES_URL}
5966) to store strings with \`window.Meticulous.record.recordCustomData(key, value)\` and read them during replay with \`window.Meticulous.replay.retrieveCustomData(key)\`.
5967
5968Both keys and values must be strings. Serialize objects with \`JSON.stringify\` before recording and parse them after retrieval. Recording the same key again overwrites its previous value; retrieval returns \`null\` if the key was not recorded.
5969
5970{% callout type="warning" title="Replay timing" %}
5971Custom values do not replay file-selection events or populate \`input.files\`. Restoring a preview in a mount effect can display it before the user originally selected a file, and only the last value for a key is retained. For image previews, CSV processing, or repeated selections that must happen at the recorded time, use Approach 3 below.
5972{% /callout %}
5973
5974{% callout type="warning" title="Size limits" %}
5975The default limits for a custom value in the initialized recorder are **1,000,000 characters in production**, **3,000,000 when the environment is unknown**, and **20,000,000 in non-production**. These limits apply to the stored string, not the original file size; base64 data URLs are larger than the original file. Recorder configuration can override the defaults. Check the returned \`{ success }\` result, as shown under Error Handling below.
5976{% /callout %}
5977
5978### Storing an Image or Text File
5979
5980Call this helper from your existing file-selection handler. It records the contents after reading the file:
5981
5982\`\`\`typescript
5983async function recordFileContents(file: File) {
5984  const dataUrl = await new Promise<string>((resolve, reject) => {
5985    const reader = new FileReader();
5986    reader.onload = () => resolve(reader.result as string);
5987    reader.onerror = () => reject(reader.error);
5988    reader.readAsDataURL(file);
5989  });
5990
5991  const meticulous = window.Meticulous;
5992  if (meticulous && !meticulous.isRunningAsTest) {
5993    return meticulous.record.recordCustomData('imagePreviewData', dataUrl);
5994  }
5995}
5996\`\`\`
5997
5998At the point where your application needs the saved data during replay:
5999
6000\`\`\`typescript
6001if (window.Meticulous?.isRunningAsTest) {
6002  const dataUrl = window.Meticulous.replay.retrieveCustomData('imagePreviewData');
6003  if (dataUrl !== null) {
6004    processFile(dataUrl);
6005  }
6006}
6007\`\`\`
6008
6009For text such as CSV content, store the text directly instead of a data URL:
6010
6011\`\`\`typescript
6012const meticulous = window.Meticulous;
6013if (meticulous && !meticulous.isRunningAsTest) {
6014  meticulous.record.recordCustomData('csvData', csvText);
6015}
6016
6017if (meticulous?.isRunningAsTest) {
6018  const csvText = meticulous.replay.retrieveCustomData('csvData');
6019  if (csvText !== null) {
6020    processCsv(csvText);
6021  }
6022}
6023\`\`\`
6024
6025### Storing File Metadata with Contents
6026
6027Serialize the data and metadata into one string:
6028
6029\`\`\`typescript
6030const meticulous = window.Meticulous;
6031if (meticulous && !meticulous.isRunningAsTest) {
6032  meticulous.record.recordCustomData('uploadedFile', JSON.stringify({
6033    dataUrl,
6034    fileName: file.name,
6035    fileType: file.type,
6036  }));
6037}
6038
6039if (meticulous?.isRunningAsTest) {
6040  const serializedFile = meticulous.replay.retrieveCustomData('uploadedFile');
6041  if (serializedFile !== null) {
6042    const storedFile = JSON.parse(serializedFile);
6043    processFile(storedFile.dataUrl);
6044  }
6045}
6046\`\`\`
6047
6048---
6049
6050## Approach 3: Custom Event API (Advanced)
6051
6052**When to use**: Your app displays a preview or processes file contents when a file is selected, including when users select multiple files in succession.
6053
6054Use the [custom event API](${o.USE_CUSTOM_EVENT_API_URL}) to record each completed file read and deliver its data at the recorded time during replay. Record with \`record.recordCustomEvent(type, serializedData)\` and subscribe with \`replay.addCustomEventListener(type, callback)\`. The callback receives the serialized string.
6055
6056Register the listener before the recorded event occurs. In this React example, the effect ignores callbacks after cleanup because the API does not provide a listener-removal method. Use an event type specific to this upload flow so other upload components do not process the same events.
6057
6058\`\`\`typescript
6059function AdvancedFileUpload() {
6060  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6061    if (window.Meticulous?.isRunningAsTest) return;
6062
6063    const file = event.target.files?.[0];
6064    if (!file) return;
6065
6066    const reader = new FileReader();
6067    reader.onload = (e) => {
6068      const payload = {
6069        fileName: file.name,
6070        fileType: file.type,
6071        fileData: e.target?.result as string,
6072      };
6073
6074      const meticulous = window.Meticulous;
6075      if (meticulous && !meticulous.isRunningAsTest) {
6076        const result = meticulous.record.recordCustomEvent(
6077          'PROFILE_IMAGE_READ',
6078          JSON.stringify(payload),
6079        );
6080        if (!result.success) {
6081          console.warn('File contents could not be recorded for replay');
6082        }
6083      }
6084
6085      // Run the same application logic during recording and replay
6086      processUploadedFile(payload);
6087    };
6088    reader.readAsDataURL(file);
6089  };
6090
6091  useEffect(() => {
6092    if (!window.Meticulous?.isRunningAsTest) return;
6093
6094    let active = true;
6095    window.Meticulous.replay.addCustomEventListener(
6096      'PROFILE_IMAGE_READ',
6097      (serializedData) => {
6098        if (active) {
6099          return processUploadedFile(JSON.parse(serializedData));
6100        }
6101      },
6102    );
6103
6104    return () => { active = false; };
6105  }, []);
6106
6107  return <input type="file" onChange={handleFileChange} />
6107;
6108}
6109\`\`\`
6110
6111---
6112
6113## Complete Integration Examples
6114
6115### With FormData and Fetch
6116
6117\`\`\`typescript
6118async function uploadFile(file: File | null) {
6119  // Skip file validation during tests
6120  if (!window.Meticulous?.isRunningAsTest && !file) {
6121    throw new Error('No file selected');
6122  }
6123
6124  const formData = new FormData();
6125  if (file) {
6126    formData.append('file', file);
6127    formData.append('userId', getCurrentUserId());
6128    formData.append('uploadType', 'document');
6129  }
6130
6131  const response = await fetch('/api/v1/upload', {
6132    method: 'POST',
6133    headers: {
6134      'Authorization': \`Bearer \${getAuthToken()}\`,
6135    },
6136    body: formData,
6137  });
6138
6139  if (!response.ok) {
6140    throw new Error('Upload failed');
6141  }
6142
6143  // Backend response is mocked during replay
6144  const result = await response.json();
6145  return result.fileUrl;
6146}
6147\`\`\`
6148
6149### With XMLHttpRequest Progress Tracking
6150
6151\`\`\`typescript
6152function uploadWithProgress(file: File | null, onProgress: (percent: number) => void) {
6153  return new Promise((resolve, reject) => {
6154    // Skip validation during tests
6155    if (!window.Meticulous?.isRunningAsTest && !file) {
6156      reject(new Error('No file selected'));
6157      return;
6158    }
6159
6160    const xhr = new XMLHttpRequest();
6161
6162    xhr.upload.addEventListener('progress', (e) => {
6163      if (e.lengthComputable) {
6164        const percentComplete = (e.loaded / e.total) * 100;
6165        onProgress(percentComplete);
6166      }
6167    });
6168
6169    xhr.addEventListener('load', () => {
6170      if (xhr.status === 200) {
6171        resolve(JSON.parse(xhr.responseText));
6172      } else {
6173        reject(new Error('Upload failed'));
6174      }
6175    });
6176
6177    xhr.addEventListener('error', () => reject(new Error('Network error')));
6178
6179    xhr.open('POST', '/api/upload');
6180
6181    const formData = new FormData();
6182    if (file) {
6183      formData.append('file', file);
6184    }
6185
6186    xhr.send(formData);
6187  });
6188}
6189\`\`\`
6190
6191---
6192
6193## Common Patterns
6194
6195### Multiple Files from Single Input
6196
6197\`\`\`typescript
6198function MultiFileUpload() {
6199  const [files, setFiles] = useState<File[]>([]);
6200
6201  const handleFilesChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6202    const selectedFiles = Array.from(event.target.files || []);
6203    setFiles(selectedFiles);
6204  };
6205
6206  const handleUpload = async () => {
6207    // Skip validation during tests
6208    if (!window.Meticulous?.isRunningAsTest && files.length === 0) {
6209      alert('Please select at least one file');
6210      return;
6211    }
6212
6213    const formData = new FormData();
6214    files.forEach((file, index) => {
6215      formData.append(\`file\${index}\`, file);
6216    });
6217
6218    await fetch('/api/upload-multiple', {
6219      method: 'POST',
6220      body: formData,
6221    });
6222  };
6223
6224  return (
6225    <div>
6226      <input type="file" multiple onChange={handleFilesChange} />
6227      <p>{files.length} files selected</p>
6228      <button onClick={handleUpload}>Upload All</button>
6229    </div>
6230  );
6231}
6232\`\`\`
6233
6234### Conditional File Processing
6235
6236\`\`\`typescript
6237function ConditionalUpload() {
6238  const [shouldValidate, setShouldValidate] = useState(false);
6239
6240  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6241    const file = event.target.files?.[0];
6242    if (!file) return;
6243
6244    if (shouldValidate && !window.Meticulous?.isRunningAsTest) {
6245      // Only process file if validation is enabled and not in test
6246      const reader = new FileReader();
6247      reader.onload = (e) => {
6248        validateFileContents(e.target?.result);
6249      };
6250      reader.readAsText(file);
6251    } else {
6252      // Skip to upload
6253      uploadFile(file);
6254    }
6255  };
6256
6257  return (
6258    <div>
6259      <label>
6260        <input
6261          type="checkbox"
6262          checked={shouldValidate}
6263          onChange={(e) => setShouldValidate(e.target.checked)}
6264        />
6265        Validate file contents before upload
6266      </label>
6267      <input type="file" onChange={handleFileChange} />
6268    </div>
6269  );
6270}
6271\`\`\`
6272
6273---
6274
6275## Error Handling
6276
6277### File Too Large for Custom Values API
6278
6279\`\`\`typescript
6280function SmartFileUpload() {
6281  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6282    const file = event.target.files?.[0];
6283    if (!file) return;
6284
6285    const reader = new FileReader();
6286    reader.onload = (e) => {
6287      const dataUrl = e.target?.result as string;
6288
6289      const meticulous = window.Meticulous;
6290      if (meticulous && !meticulous.isRunningAsTest) {
6291        const result = meticulous.record.recordCustomData('fileData', dataUrl);
6292        if (!result.success) {
6293          console.warn('File contents could not be recorded for replay');
6294          // Handle missing replay data in your application.
6295        }
6296      }
6297
6298      processFile(dataUrl);
6299    };
6300    reader.readAsDataURL(file);
6301  };
6302
6303  return <input type="file" onChange={handleFileChange} />;
6304}
6305\`\`\`
6306
6307### Handling Unsupported File Types
6308
6309\`\`\`typescript
6310function TypeSafeUpload() {
6311  const ALLOWED_TYPES = {
6312    'image/jpeg': ['.jpg', '.jpeg'],
6313    'image/png': ['.png'],
6314    'application/pdf': ['.pdf'],
6315  };
6316
6317  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6318    const file = event.target.files?.[0];
6319    if (!file) return;
6320
6321    if (!window.Meticulous?.isRunningAsTest) {
6322      if (!Object.keys(ALLOWED_TYPES).includes(file.type)) {
6323        alert(\`Unsupported file type: \${file.type}\`);
6324        event.target.value = ''; // Clear input
6325        return;
6326      }
6327    }
6328
6329    uploadFile(file);
6330  };
6331
6332  const acceptString = Object.values(ALLOWED_TYPES).flat().join(',');
6333
6334  return <input type="file" accept={acceptString} onChange={handleFileChange} />;
6335}
6336\`\`\`
6337
6338---
6339
6340## Summary
6341
6342**Most apps should use Approach 1** (skip validation during tests) because:
6343- Simple and requires minimal code changes
6344- Works with Meticulous' network stubbing
6345- Tests the complete user flow including backend responses
6346- No file size limitations
6347
6348**Use Approach 2** (store file contents) only when:
6349- You need to test frontend file processing logic
6350- The serialized contents fit within the recorder's configured limit
6351- Your app consumes the saved value at a known point during replay
6352
6353**Use Approach 3** (custom event API) for:
6354- Image previews or file processing that must run at the recorded time
6355- Repeated file selections whose individual contents must be preserved
6356- Integration with existing custom event systems
6357
6358For more details on the Meticulous API, see the [window.Meticulous object documentation](${o.METICULOUS_WINDOW_OBJECT_URL}).
6359`,eL=`---
6360{
6361  "title": "Companion Assets (Advanced)"
6362}
6363---
6364
6365# {% $frontmatter.title %}
6366
6367{% callout type="info" title="Companion assets are only relevant to the cloud-compute (tunnel) workflow" %}
6368If you can build your app as a Docker image, prefer the \`upload-container\` action — Meticulous serves your app directly from the uploaded image, so there is no tunnel and no need for companion assets. The companion-assets pattern below only applies when you're using the tunnel-based \`cloud-compute\` action.
6369{% /callout %}
6370
6371## What are Companion Assets?
6372
6373Companion assets allow you to serve static files alongside your running application during Meticulous tests. When a request matches a specific pattern, Meticulous serves the file from a local folder instead of proxying it through the secure tunnel.
6374
6375This is an advanced feature that solves specific performance and deployment challenges.
6376
6377---
6378
6379## When to Use Companion Assets
6380
6381### Next.js Applications
6382
6383**Problem**: Next.js apps generate static assets in the \`.next/static/\` folder during build. These assets include JavaScript bundles, CSS files, and other resources. When testing a Next.js app locally, these assets need to be available at the correct paths.
6384
6385**Solution**: Upload the \`.next/static/\` folder as companion assets and configure Meticulous to serve requests to \`/_next/static/\` from this folder.
6386
6387### Large Static Assets
6388
6389**Problem**: Your app has large static assets (images, fonts, videos) that slow down the secure tunnel. Transferring large files through the tunnel adds latency and can timeout.
6390
6391**Solution**: Upload these static assets as companion assets so Meticulous serves them directly, bypassing the tunnel.
6392
6393### CDN-Hosted Assets During Recording
6394
6395**Problem**: During session recording, your app loads assets from a CDN (e.g., \`https://cdn.example.com/assets/\`). During testing, you want to serve these assets locally instead of from the CDN.
6396
6397**Solution**: Download the CDN assets locally, upload them as companion assets, and configure Meticulous to intercept CDN requests and serve them from your local folder.
6398
6399### Multi-Server Applications
6400
6401**Problem**: Your application is served by multiple servers (e.g., main app on \`:3000\`, assets server on \`:8080\`). During tests, you want to consolidate asset serving.
6402
6403**Solution**: Use companion assets to serve assets that would normally come from a separate server.
6404
6405---
6406
6407## How Companion Assets Work
6408
6409When you configure companion assets, Meticulous:
6410
64111. **Uploads** the specified local folder to cloud storage
64122. **Starts** a static file server in the test environment with your assets
64133. **Intercepts** requests matching the regex pattern
64144. **Redirects** matching requests to the local asset server instead of proxying through the tunnel
6415
6416### Request Routing Example
6417
6418Without companion assets:
6419\`\`\`
6420Browser → /_next/static/chunks/main.js → Tunnel → Your App (localhost:3000)
6421\`\`\`
6422
6423With companion assets:
6424\`\`\`
6425Browser → /_next/static/chunks/main.js → Companion Assets Server → Uploaded files
6426\`\`\`
6427
6428---
6429
6430## Configuration
6431
6432Companion assets require **two parameters**, and you must provide **both or neither**:
6433
6434### 1. \`companion-assets-folder\`
6435
6436The path to a local folder containing the static files to upload.
6437
6438- Must be a relative or absolute path on your CI runner
6439- The entire folder contents are uploaded
6440- Folder structure is preserved
6441
6442### 2. \`companion-assets-regex\`
6443
6444A regular expression pattern to match request URLs that should be served from companion assets.
6445
6446- Matches against the request **pathname** (e.g., \`/_next/static/chunks/main.js\`)
6447- Should typically start with \`^\` to match from the beginning
6448- Only GET and HEAD requests are intercepted
6449- Case-sensitive by default
6450
6451{% callout type="warning" title="Both Parameters Required" %}
6452You must provide both \`companion-assets-folder\` and \`companion-assets-regex\`, or neither. Providing only one will result in an error.
6453{% /callout %}
6454
6455---
6456
6457## Complete Examples
6458
6459### Next.js Application
6460
6461Next.js apps are the most common use case for companion assets.
6462
6463#### GitHub Actions Workflow
6464
6465\`\`\`yaml
6466${g}
6467
6468      - name: Setup Node.js
6469        uses: actions/setup-node@v4
6470        with:
6471          node-version: "24"
6472          cache: pnpm
6473
6474      - name: Install dependencies
6475        run: pnpm install --frozen-lockfile
6476
6477      - name: Build Next.js app
6478        run: pnpm build
6479
6480      - name: Prepare companion assets
6481        run: |
6482          # Create companion assets folder
6483          mkdir -p companion-assets/_next
6484          # Copy Next.js static assets
6485          cp -r .next/static companion-assets/_next/
6486
6487      - name: Serve Next.js app
6488        run: |
6489          pnpm start &
6490          sleep 5
6491
6492      - name: Run Meticulous tests
6493        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6494        with:
6495          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6496          app-url: "http://localhost:3000"
6497          companion-assets-folder: "companion-assets"
6498          companion-assets-regex: "^/_next/static/"
6499\`\`\`
6500
6501**Key Points:**
6502- Build the Next.js app first (\`pnpm build\`)
6503- Copy \`.next/static/\` to \`companion-assets/_next/\` (preserves path structure)
6504- Regex \`^/_next/static/\` matches all requests starting with \`/_next/static/\`
6505- Folder structure in \`companion-assets\` matches URL structure
6506
6507#### CLI Usage
6508
6509\`\`\`bash
6510# Build the app
6511pnpm build
6512
6513# Prepare companion assets
6514mkdir -p companion-assets/_next
6515cp -r .next/static companion-assets/_next/
6516
6517# Start the app
6518pnpm start &
6519
6520# Run tests with companion assets
6521npx @alwaysmeticulous/cli ci run-with-tunnel \\
6522  --apiToken="$METICULOUS_API_TOKEN" \\
6523  --appUrl="http://localhost:3000" \\
6524  --companionAssetsFolder="companion-assets" \\
6525  --companionAssetsRegex="^/_next/static/"
6526\`\`\`
6527
6528### Vite App with CDN Assets
6529
6530If your Vite app loads assets from a CDN during production but you want to test with local assets:
6531
6532\`\`\`yaml
6533      - name: Build Vite app
6534        run: pnpm build
6535
6536      - name: Prepare companion assets
6537        run: |
6538          # Copy built assets
6539          mkdir -p companion-assets/assets
6540          cp -r dist/assets/* companion-assets/assets/
6541
6542      - name: Serve app
6543        run: |
6544          pnpm preview &
6545          sleep 3
6546
6547      - name: Run Meticulous tests
6548        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6549        with:
6550          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6551          app-url: "http://localhost:4173"
6552          companion-assets-folder: "companion-assets"
6553          companion-assets-regex: "^/assets/"
6554\`\`\`
6555
6556### Multiple Asset Patterns
6557
6558If you need to serve assets from multiple paths:
6559
6560\`\`\`yaml
6561      - name: Prepare companion assets
6562        run: |
6563          mkdir -p companion-assets
6564          # Copy static assets
6565          cp -r public/static companion-assets/static
6566          # Copy built bundles
6567          cp -r dist/bundles companion-assets/bundles
6568
6569      - name: Run Meticulous tests
6570        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6571        with:
6572          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6573          app-url: "http://localhost:3000"
6574          companion-assets-folder: "companion-assets"
6575          # Match /static/ OR /bundles/
6576          companion-assets-regex: "^/(static|bundles)/"
6577\`\`\`
6578
6579### React App (Create React App)
6580
6581\`\`\`yaml
6582      - name: Build React app
6583        run: pnpm build
6584
6585      - name: Prepare companion assets
6586        run: |
6587          mkdir -p companion-assets
6588          # Copy static files from build output
6589          cp -r build/static companion-assets/static
6590
6591      - name: Serve app
6592        run: |
6593          npx serve -s build -p 3000 &
6594          sleep 3
6595
6596      - name: Run Meticulous tests
6597        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6598        with:
6599          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6600          app-url: "http://localhost:3000"
6601          companion-assets-folder: "companion-assets"
6602          companion-assets-regex: "^/static/"
6603\`\`\`
6604
6605---
6606
6607## Folder Structure Requirements
6608
6609The folder structure inside \`companion-assets-folder\` must match the URL paths.
6610
6611### Example 1: Next.js
6612
6613**Request URL**: \`http://localhost:3000/_next/static/chunks/main.js\`
6614
6615**Regex**: \`^/_next/static/\`
6616
6617**Folder structure**:
6618\`\`\`
6619companion-assets/
6620└── _next/
6621    └── static/
6622        └── chunks/
6623            └── main.js
6624\`\`\`
6625
6626The pathname \`/_next/static/chunks/main.js\` maps to \`companion-assets/_next/static/chunks/main.js\`.
6627
6628### Example 2: Generic Assets
6629
6630**Request URL**: \`http://localhost:3000/assets/images/logo.png\`
6631
6632**Regex**: \`^/assets/\`
6633
6634**Folder structure**:
6635\`\`\`
6636companion-assets/
6637└── assets/
6638    └── images/
6639        └── logo.png
6640\`\`\`
6641
6642### Example 3: Root-Level Assets
6643
6644**Request URL**: \`http://localhost:3000/public/font.woff2\`
6645
6646**Regex**: \`^/public/\`
6647
6648**Folder structure**:
6649\`\`\`
6650companion-assets/
6651└── public/
6652    └── font.woff2
6653\`\`\`
6654
6655---
6656
6657## Regex Pattern Guide
6658
6659### Basic Patterns
6660
6661| Use Case | Regex | Matches |
6662|----------|-------|---------|
6663| Next.js static assets | \`^/_next/static/\` | \`/_next/static/chunks/main.js\`<br/>\`/_next/static/css/app.css\` |
6664| Assets folder | \`^/assets/\` | \`/assets/images/logo.png\`<br/>\`/assets/fonts/font.woff\` |
6665| Static folder | \`^/static/\` | \`/static/js/bundle.js\`<br/>\`/static/css/main.css\` |
6666| Multiple folders | \`^/(assets|static)/\` | \`/assets/logo.png\`<br/>\`/static/main.js\` |
6667| Specific file types | \`^/.*\\.(png|jpg|woff2)$\` | \`/images/logo.png\`<br/>\`/fonts/font.woff2\` |
6668
6669### Advanced Patterns
6670
6671**Match all .js files in /dist/**:
6672\`\`\`
6673^/dist/.*\\.js$
6674\`\`\`
6675
6676**Match versioned assets**:
6677\`\`\`
6678^/assets/v[0-9]+/
6679\`\`\`
6680Matches: \`/assets/v1/main.js\`, \`/assets/v2/app.css\`
6681
6682**Match specific subdirectories**:
6683\`\`\`
6684^/static/(js|css|media)/
6685\`\`\`
6686Matches: \`/static/js/main.js\`, \`/static/css/app.css\`, \`/static/media/logo.png\`
6687
6688---
6689
6690## Troubleshooting
6691
6692### Assets Not Loading
6693
6694**Symptom**: Your app shows missing resources or broken styles during tests.
6695
6696**Possible Causes**:
6697
66981. **Regex doesn't match requests**
6699   - Check the browser network tab in test results
6700   - Verify the exact pathname being requested
6701   - Test your regex pattern at [regex101.com](https://regex101.com)
6702
6703   \`\`\`bash
6704   # Example: If requests are for /_next/static/chunks/main-abc123.js
6705   # This regex won't match (missing trailing slash):
6706   companion-assets-regex: "^/_next/static"
6707
6708   # This will match:
6709   companion-assets-regex: "^/_next/static/"
6710   \`\`\`
6711
67122. **Folder structure doesn't match URL structure**
6713   - Request: \`/_next/static/main.js\`
6714   - Correct: \`companion-assets/_next/static/main.js\`
6715   - Incorrect: \`companion-assets/static/main.js\`
6716
67173. **Files not copied to companion assets folder**
6718   - Verify files exist: \`ls -la companion-assets/_next/static/\`
6719   - Check your build output directory
6720   - Ensure copy commands run after build
6721
6722### Wrong Files Served
6723
6724**Symptom**: Meticulous serves incorrect or outdated files.
6725
6726**Solution**: Ensure you're copying the correct build output.
6727
6728\`\`\`bash
6729# Bad: Copies from source instead of build output
6730cp -r src/assets companion-assets/
6731
6732# Good: Copies from build output
6733cp -r .next/static companion-assets/_next/
6734\`\`\`
6735
6736### Assets Upload Too Large
6737
6738**Symptom**: Companion assets upload times out or fails.
6739
6740**Solutions**:
6741
67421. **Exclude unnecessary files**:
6743   \`\`\`bash
6744   # Only copy necessary files
6745   cp -r .next/static companion-assets/_next/
6746   # Don't copy source maps in production
6747   find companion-assets -name "*.map" -delete
6748   \`\`\`
6749
67502. **Use more specific regex**:
6751   \`\`\`yaml
6752   # Instead of matching all assets
6753   companion-assets-regex: "^/assets/"
6754
6755   # Match only large files (images, fonts)
6756   companion-assets-regex: "^/assets/.*\\.(png|jpg|woff2|woff|ttf)$"
6757   \`\`\`
6758
6759### Debugging Request Matching
6760
6761To see which requests are being intercepted, you can add the \`--printRequests\` flag when using the CLI:
6762
6763\`\`\`bash
6764npx @alwaysmeticulous/cli ci run-with-tunnel \\
6765  --apiToken="$METICULOUS_API_TOKEN" \\
6766  --appUrl="http://localhost:3000" \\
6767  --companionAssetsFolder="companion-assets" \\
6768  --companionAssetsRegex="^/_next/static/" \\
6769  --printRequests
6770\`\`\`
6771
6772For GitHub Actions, you can add the \`meticulous-debug\` label to your PR to keep the secure tunnel open and access detailed logs.
6773
6774---
6775
6776## Performance Considerations
6777
6778### When Companion Assets Help
6779
6780- **Large files**: Images, videos, fonts (>100KB)
6781- **Many files**: Hundreds of small assets loaded per page
6782- **Slow builds**: Next.js apps with large static asset folders
6783- **Bandwidth limits**: CI environments with limited egress
6784
6785### When Companion Assets May Not Help
6786
6787- **Small apps**: Apps with minimal static assets (<10MB total)
6788- **Fast tunnels**: If tunnel performance is already good
6789- **Dynamic assets**: Assets that change based on runtime logic
6790
6791### Measuring Impact
6792
6793Compare test run times with and without companion assets:
6794
67951. Run tests without companion assets
67962. Note the total test run duration
67973. Enable companion assets
67984. Compare the new duration
6799
6800Typical improvements: 10-30% faster test runs for Next.js apps with large static folders.
6801
6802---
6803
6804## CLI Reference
6805
6806### \`ci run-with-tunnel\`
6807
6808\`\`\`bash
6809npx @alwaysmeticulous/cli ci run-with-tunnel \\
6810  --apiToken="<token>" \\
6811  --appUrl="<url>" \\
6812  --companionAssetsFolder="<folder>" \\
6813  --companionAssetsRegex="<regex>"
6814\`\`\`
6815
6816**Parameters**:
6817- \`--companionAssetsFolder\`: Path to local folder (relative or absolute)
6818- \`--companionAssetsRegex\`: Regex pattern (must be properly escaped)
6819
6820### \`ci start-tunnel\`
6821
6822When testing tunnel setup locally:
6823
6824\`\`\`bash
6825npx @alwaysmeticulous/cli ci start-tunnel --port=3000
6826\`\`\`
6827
6828---
6829
6830## GitHub Actions Reference
6831
6832### \`cloud-compute\` Action
6833
6834\`\`\`yaml
6835- uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6836  with:
6837    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6838    app-url: "http://localhost:3000"
6839    companion-assets-folder: "companion-assets"  # Required if regex provided
6840    companion-assets-regex: "^/_next/static/"    # Required if folder provided
6841\`\`\`
6842
6843**Input Types**:
6844- \`companion-assets-folder\`: String (path)
6845- \`companion-assets-regex\`: String (regex pattern)
6846
6847**Defaults**: Both default to empty string (feature disabled)
6848
6849---
6850
6851## Common Patterns
6852
6853### Monorepo with Multiple Next.js Apps
6854
6855\`\`\`yaml
6856      - name: Build apps
6857        run: |
6858          pnpm build:app1
6859          pnpm build:app2
6860
6861      - name: Prepare companion assets for both apps
6862        run: |
6863          mkdir -p companion-assets/app1/_next
6864          mkdir -p companion-assets/app2/_next
6865          cp -r apps/app1/.next/static companion-assets/app1/_next/
6866          cp -r apps/app2/.next/static companion-assets/app2/_next/
6867
6868      - name: Run tests for App 1
6869        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6870        with:
6871          api-token: \${{ secrets.APP1_API_TOKEN }}
6872          app-url: "http://localhost:3000"
6873          companion-assets-folder: "companion-assets/app1"
6874          companion-assets-regex: "^/_next/static/"
6875
6876      - name: Run tests for App 2
6877        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6878        with:
6879          api-token: \${{ secrets.APP2_API_TOKEN }}
6880          app-url: "http://localhost:4000"
6881          companion-assets-folder: "companion-assets/app2"
6882          companion-assets-regex: "^/_next/static/"
6883\`\`\`
6884
6885### Conditional Companion Assets
6886
6887\`\`\`yaml
6888      - name: Prepare companion assets (only for Next.js)
6889        if: \${{ env.FRAMEWORK == 'nextjs' }}
6890        run: |
6891          mkdir -p companion-assets/_next
6892          cp -r .next/static companion-assets/_next/
6893
6894      - name: Run Meticulous tests
6895        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6896        with:
6897          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6898          app-url: "http://localhost:3000"
6899          companion-assets-folder: \${{ env.FRAMEWORK == 'nextjs' && 'companion-assets' || '' }}
6900          companion-assets-regex: \${{ env.FRAMEWORK == 'nextjs' && '^/_next/static/' || '' }}
6901\`\`\`
6902
6903---
6904
6905## Summary
6906
6907**Use companion assets when**:
6908- You have a Next.js application
6909- You have large static assets that slow down the tunnel
6910- Assets are served from a CDN during recording but should be local during tests
6911- You need to consolidate assets from multiple servers
6912
6913**Configuration requirements**:
6914- Both \`companion-assets-folder\` and \`companion-assets-regex\` must be provided
6915- Folder structure must match URL path structure
6916- Regex must accurately match the requests you want to intercept
6917
6918**Common mistakes to avoid**:
6919- Mismatched folder structure and URL paths
6920- Regex that doesn't match actual request paths
6921- Forgetting to build the app before copying assets
6922- Copying source files instead of build output
6923`,eP=`---
6924{
6925  "title": "Using the Custom Event API"
6926}
6927---
6928
6929# {% $frontmatter.title %}
6930
6931The Custom Event API allows you to record and replay custom events in your application with deterministic timing.
6932This can be useful if your application relies on external services or web APIs which Meticulous does not mock out by default
6933(e.g. browser extensions, EventSource, etc).
6934
6935- During recording, you can emit custom events with associated data using the \`window.Meticulous?.record?.recordCustomEvent\` method.
6936- During replay, Meticulous will emit custom events at the same time they occurred during the recording.
6937You can listen for these custom events using the \`window.Meticulous?.replay?.addCustomEventListener\` method:
6938
6939## Examples
6940
6941{% anchor id="orientation-example" /%}
6942### Example: Extend Meticulous to support device orientation events
6943
6944\`\`\`typescript
6945const handleOrientationEvent = (opts) => {
6946  // Your existing code that handles the orientation event
6947};
6948
6949if (window.Meticulous?.replay) {
6950  window.Meticulous.replay.addCustomEventListener("deviceOrientationChanged",
6951      (serializedData) => handleOrientationEvent(JSON.parse(serializedData))
6952    );
6953} else {
6954  window.addEventListener(
6955    "deviceorientation",
6956    (event) => {
6957        window.Meticulous?.record?.recordCustomEvent(
6958          "deviceOrientationChanged",
6959          JSON.stringify({ rotation: event.alpha })
6960        );
6961        handleOrientationEvent({ rotation: event.alpha });
6962    }
6963  );
6964}
6965\`\`\`
6966
6967{% anchor id="message-example" /%}
6968### Example: Simulate messages from a 3rd party iframe, without rendering that iframe at test time
6969
6970Imagine you have a 3rd party iframe you process messages from, but don't actually want to run or test the 3rd party code. Instead you wish to test that your
6971application correctly handles the user flow around the iframe, including correctly handling any messages received from the iframe.
6972
6973In this case your code may look like this:
6974
6975\`\`\`typescript
6976const iframe = createIFrameAndAddToDOM();
6977iframe.addEventListener("message", handleMessageFromIframe);
6978\`\`\`
6979
6980You can use the custom event API to configure Meticulous to record any messages received from the iframe at recording time, and
6981then simulate those messages at replay time, without actually rendering the iframe at all at replay time:
6982
6983\`\`\`typescript
6984if (window.Meticulous?.replay) {
6985   window.Meticulous.replay.addCustomEventListener(
6986    "message-from-my-iframe",
6987    (serializedData) => handleMessageFromIframe(deserialize(serializedData))
6988  );
6989} else {
6990   const iframe = createIFrameAndAddToDOM();
6991   iframe.addEventListener("message", handleMessageFromIframe);
6992   if (window.Meticulous?.record) {
6993       iframe.addEventListener(
6994        "message",
6995        msg => window.Meticulous.record.recordCustomEvent(
6996          "message-from-my-iframe",
6997          serialize(msg)
6998        )
6999      );
7000   }
7001}
7002\`\`\`
7003
7004## Best Practices
7005
7006- window.Meticulous?.record will only be defined at record time and window.Meticulous?.replay will only be defined at replay time,
7007so you don't need to do a special check to see if you're in recording or replay mode
7008- Use descriptive event names that clearly indicate their purpose
7009- Keep event data simple and serializable
7010- Handle cases where the Meticulous API might not be available (e.g., in development)
7011
7012## TypeScript Types
7013
7014For TypeScript type definitions for the \`window.Meticulous\` object, see [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}).
7015`,eO="local-mocks",eN="approval",eD=`---
7016{
7017  "title": "Recorder Developer Tools"
7018}
7019---
7020
7021# {% $frontmatter.title %}
7022
7023The recorder ships with an in-browser developer overlay that you can opt into on
7024any non-production environment. It gives you two things directly inside the page
7025the recorder is running on:
7026
70271. **Manual session selection** — mark the session you are currently recording as
7028   always-run in CI, without leaving your app.
70292. **Local mocks** — switch between mock network scenarios served by a local
7030   [Meticulous Local Mocks](#${eO}) MCP server while you iterate
7031   on UI.
7032
7033It appears as a small dark logo in a corner of the page that you can drag and
7034click to expand. The overlay never loads in production environments and the
7035recorder snippet has to be installed for it to show up — see
7036[Install the Recorder](${o.INSTALL_RECORDER_URL}) if you don't have it set up yet.
7037
7038{% anchor id="enable" /%}
7039## Enabling the developer tools
7040
7041Open the browser DevTools console on any page where the recorder is running and
7042run:
7043
7044\`\`\`javascript
7045window.Meticulous.enableDeveloperTools();
7046\`\`\`
7047
7048This sets a local flag and reloads the page. From then on, every time the
7049recorder loads on any non-production origin in this browser, the developer
7050overlay is loaded too.
7051
7052The overlay does **not** load if the recorder script reports the page as a
7053production environment (\`data-is-production-environment="true"\` on the
7054recorder snippet tag). This is intentional — the manual selection and local
7055mocks features are development tooling, not user-facing features.
7056
7057{% anchor id="manual-selection" /%}
7058## Manually selecting a session
7059
7060Meticulous's [automatic session selection](${o.TESTING_POOL_URL}) is the
7061recommended default for almost everyone: it continuously adapts the suite of
7062sessions executed in CI to maximize coverage of your application as it changes.
7063
7064There are still cases where you want to nail a specific user flow to the
7065suite — for example, a regression you just hand-recorded, or a flow that
7066exercises an edge case the auto-selector keeps missing. The "Manually select
7067session" button in the overlay does exactly that, without you having to leave
7068the page or open the Meticulous app.
7069
7070### How it works
7071
70721. Perform the user flow you want to capture.
70732. Click the Meticulous logo in the corner to open the overlay.
70743. Click **Manually select session**. Optionally give the session a memorable
7075   name (e.g. "Checkout — happy path") and confirm.
70764. A popup will open prompting you to approve the request in the Meticulous
7077   app — see [Approval flow](#${eN}) below.
70785. Once approved, the button switches to **Manually selected**. Click it again
7079   to remove the session from the manually-selected set.
7080
7081A small "Manage manually selected sessions" link in the overlay deep-links you
7082to the project's full manual-selection list in the Meticulous app at any time.
7083
7084### Limits
7085
7086Each project has a hard cap on the number of manually-selected sessions
7087(currently **50**) to keep CI focused. The overlay will warn you about this
7088before you confirm. Manual selection is intended for the handful of flows you
7089explicitly want to pin — use auto-selection for everything else.
7090
7091{% anchor id="${eN}" /%}
7092## The approval flow
7093
7094Because the recorder runs on your own origin (e.g. your local dev URL), rather than meticulous.ai,
7095it cannot directly speak to the Meticulous app with your session cookies. So
7096the first time you ask the overlay to mark or unmark a session, it opens a
7097small approval popup that:
7098
7099- Asks you to sign into the Meticulous app (if you aren't already).
7100- Asks you to explicitly approve giving this browser permission to
7101  mark/unmark sessions for the project.
7102
7103After you approve once, the overlay caches a short-lived bearer token in your
7104browser. Subsequent mark/unmark actions complete instantly without re-prompting,
7105until the token expires (~8 hours) or you sign out.
7106
7107The approval popup is opened synchronously inside the click handler so that
7108browser popup blockers honor it. If your browser still blocks it, allow popups
7109for the page and try again.
7110
7111{% anchor id="${eO}" /%}
7112## Local mocks
7113
7114The "Local Mocks" panel in the overlay is a switcher for mock network scenarios
7115served by the **Meticulous Local Mocks MCP server**, an opt-in tool that
7116records the network traffic of your local browsing and lets your coding agent
7117generate scenarios from it.
7118
7119When the MCP server is running and paired with the overlay, you can pick a
7120scenario from the dropdown to have the overlay intercept and replay its
7121recorded responses for the rest of the page session — useful for flicking
7122between edge cases (e.g. empty state, error state, paginated state) while you
7123iterate on the UI, without having to set up a backend to reproduce them.
7124
7125If you don't have the MCP server running, the panel shows an "Enable
7126Auto-Mocks (alpha)" button that walks you through the one-time pairing step.
7127
7128## Repositioning the overlay
7129
7130Drag the logo to any corner of the page. The overlay remembers which corner it
7131was last in (per browser tab) and snaps to it on the next page load. Because it
7132anchors to the nearest viewport corner with CSS, it stays in view even when you
7133resize the window.
7134
7135{% anchor id="disable" /%}
7136## Disabling the developer tools
7137
7138Run the following in the browser DevTools console:
7139
7140\`\`\`javascript
7141localStorage.removeItem("meticulous.developer-tools");
7142location.reload();
7143\`\`\`
7144
7145The overlay will stop loading on subsequent page loads. Your manual-selection
7146approval token and any locally-marked sessions remain on the server; the
7147overlay just won't surface them on this device.
7148`,ej=`---
7149{
7150  "title": "Custom checks"
7151}
7152---
7153
7154# {% $frontmatter.title %}
7155
7156Meticulous visual tests catch any regression that affects the functional or visual behaviour of the application as perceived by a user. By replaying real user sessions and comparing screenshots pixel by pixel, they surface unintended visual changes before they reach production, and through this also surface any behavioural changes that affect these user workflows. But not every regression is visible. A change can leave every screen looking identical while still making your app slower, chattier, or heavier to load.
7157
7158**Meticulous custom checks** let you catch those regressions too. They let you define additional rules that run on top of the same test run — passing, warning, or requiring a reviewer's acknowledgement — surfacing the regressions a screenshot comparison cannot see. Because they reuse the sessions Meticulous already replays on every commit, you get this coverage without writing or maintaining a separate suite of tests.
7159
7160The most common use case is catching **performance and resource regressions**. A refactor might double the number of API calls a page makes, an unstable prop might cause unnecessary React re-renders, or a change might introduce extra round-trips on a critical flow — all while the UI looks pixel-perfect. Custom checks let you assert on these metrics: compare each session's behaviour on the head commit against its baseline, and surface a warning when a threshold is crossed.
7161
7162Your check logic runs in your own CI pipeline and is built with the [\`@alwaysmeticulous/custom-c
7162hecks\`](https://www.npmjs.com/package/@alwaysmeticulous/custom-checks) npm package. Unlike visual regressions — which Meticulous detects the same way for everyone — every organisation has different needs when it comes to non-visual regressions: which metrics matter, what counts as an acceptable change, and where to draw the line between a warning and a failure. Running the check logic in your own CI gives you full control to encode exactly those rules, rather than forcing your requirements into a one-size-fits-all check.
7163
7164Complete, runnable example checks live in the [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-checks-examples) repository.
7165
7166## How custom checks work
7167
7168Custom checks split into three phases:
7169
71701. **Snapshot collection (during replay).** As Meticulous replays each session on the head and base deployments, it captures *snapshots* — structured data points stored alongside the replay so they can be downloaded later. Some snapshot types require no changes in your application, such as \`network-requests\` and \`react-component-renders\` (see [Built-in snapshot types](${o.CUSTOM_CHECKS_BUILT_IN_SNAPSHOT_TYPES_URL})), and you can also record your own from browser code via \`window.Meticulous.replay.recordCustomSnapshot(...)\` (see [Recording custom snapshots](${o.CUSTOM_CHECKS_RECORDING_CUSTOM_DATA_URL})).
71712. **Check computation (in your CI).** After a test run finishes, a script you own downloads the base and head snapshots, runs your comparison logic, and posts the verdicts back to Meticulous in a single API call. For example, a check might sum each session's network requests on base and head, then require a reviewer's acknowledgement if any session makes more than 20% more API calls on head than it did on base.
71723. **Reporting and acknowledgement (on the PR).** Custom checks get their own dedicated status check on the pull request — **Meticulous Custom Checks** — separate from the visual-diff check, so a non-visual regression can be surfaced without cluttering the visual results (and vice versa). Each check decides how strict to be: it can simply warn that something looks potentially off without blocking the pull request, or it can require a reviewer to explicitly acknowledge the result in the Meticulous UI before the pull request can proceed, the same way visual diffs are reviewed.
7173`,eF=`---
7174{
7175  "title": "Writing a custom check"
7176}
7177---
7178
7179# {% $frontmatter.title %}
7180
7181This guide walks through building a **network request capacity check** from scratch: a custom check that makes sure sessions on the head test run do not issue many more network requests than they did on the base.
7182
7183It is organised in four sections, mirroring what every custom check does:
7184
71851. **Read the snapshot data** captured during replay.
71862. **Compute the result** of the check by comparing base and head.
71873. **Report the result** back to Meticulous.
71884. **Set up CI** so the check runs on every PR.
7189
7190Everything lives in a single \`report.ts\` file that we build up as we go.
7191
7192A complete, runnable version of this check lives in the [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-checks-examples) repository.
7193
7194## What we will build
7195
7196Each recorded session has a baseline on the base test run — the number of meaningful HTTP requests that session normally issues. That baseline is the session's **capacity**. On every PR we compare head against base per session, and if head exceeds capacity by too much the check surfaces a warning or failure before the change merges.
7197
7198## Prerequisites
7199
7200Before following this guide, make sure you have:
7201
7202- A Meticulous project with [CI tests set up](${o.CI_SETUP_URL}) so every PR produces head and base test runs.
7203- Custom checks enabled for your project. Contact the Meticulous team to enable the custom checks.
7204- Your Meticulous API token.
7205
7206## Pick an example test run to develop against
7207
7208Before writing any code, choose a **completed test run** to use as a running example while you build the reporter. You will point the script at it repeatedly to check that your filtering, thresholds, and report read the way you expect. Grab its id from the test run's URL in the Meticulous app — \`https://app.meticulous.ai/projects/<org>/<project>/test-runs/<testRunId>\`.
7209
7210It helps to pick a test run that actually exhibits the regression you are checking for, so you can confirm the check *fires*, not just that it runs. For a network request capacity check, a simple way to produce one is to open a PR that deliberately increases network traffic — for example, adding a short polling interval that re-fetches an endpoint — and use the test run associated with that PR as your example. Once the check is working, you can drop the PR.
7211
7212## 1. Read the snapshot data
7213
7214### Set up the reporter project
7215
7216A custom check is just a small Node script — the **reporter** — that you run after a Meticulous test run finishes. It talks to Meticulous through the published [\`@alwaysmeticulous/custom-c
7216hecks\`](https://www.npmjs.com/package/@alwaysmeticulous/custom-checks) SDK, which handles authentication, resolving test runs, downloading data, and posting results.
7217
7218Create a dedicated directory (for example \`custom-checks/\` at the repo root) with its own \`package.json\` and a single \`report.ts\` we will build up as we go:
7219
7220\`\`\`json
7221{
7222  "name": "my-custom-checks",
7223  "private": true,
7224  "scripts": {
7225    "report": "ts-node report.ts"
7226  },
7227  "dependencies": {
7228    "@alwaysmeticulous/custom-checks": "^2.296.0"
7229  },
7230  "devDependencies": {
7231    "ts-node": "^10.8.1",
7232    "typescript": "^5.9.3"
7233  }
7234}
7235\`\`\`
7236
7237Use the latest [\`@alwaysmeticulous/custom-checks\`](https://www.npmjs.com/package/@alwaysmeticulous/custom-checks) release from [npm](https://www.npmjs.com/package/@alwaysmeticulous/custom-checks) — the version above is current at the time of writing. Install dependencies with your package manager (\`npm install\`, \`pnpm install\`, etc.).
7238
7239The [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-checks-examples) repo has this project ready to run if you'd rather skip the boilerplate.
7240
7241### Resolve a test run
7242
7243Before computing anything, get a minimal \`report.ts\` running that connects to Meticulous and resolves your example test run. This confirms your token and SDK setup work before you add any check logic.
7244
7245Two SDK functions get us started:
7246
7247- \`createClient(...)\` opens an authenticated connection to Meticulous from your API token. Every other SDK call takes the client it returns.
7248- \`findTestRunForCustomChecks(...)\` takes a test run id and returns the resolved run together with its **base** run (waiting for the run to finish if it is still in progress). That head/base pair is what a check compares.
7249
7250Start with all the imports we will need across the script:
7251
7252\`\`\`typescript
7253import {
7254  createClient,
7255  findTestRunForCustomChecks,
7256  findTestRunByCommitForCustomChecks,
7257  getSnapshotsFromTestRun,
7258  reportCustomCheckResults,
7259  type MeticulousClient,
7260  type Snapshot,
7261  type CustomCheckVerdict,
7262  type ReportedCustomCheckResult,
7263} from "@alwaysmeticulous/custom-checks";
7264
7265const testRunId = process.argv[2];
7266
7267const client = createClient({
7268  apiToken: process.env.METICULOUS_API_TOKEN,
7269  appInfo: "my-app/custom-checks",
7270});
7271
7272const { testRun } = await findTestRunForCustomChecks({
7273  client,
7274  testRunId,
7275  // We are only exploring here, not reporting — don't register this run as
7276  // expecting custom checks (explained under "Report the result").
7277  skipRegisteringExpectedCustomChecks: true,
7278});
7279
7280console.log(\`Resolved test run \${testRun.id} (\${testRun.status}): \${testRun.url}\`);
7281\`\`\`
7282
7283Run it against the example test run you picked earlier:
7284
7285\`\`\`shell
7286METICULOUS_API_TOKEN=<token> pnpm run report -- <testRunId>
7287\`\`\`
7288
7289We pass \`skipRegisteringExpectedCustomChecks: true\` here because we are only exploring and will not report results — the *Report the result* section explains this flag. Everything from here on is added to this same \`report.ts\` file.
7290
7291### Understand snapshots
7292
7293The data a custom check compares comes from **snapshots**. While Meticulous replays each session — on both the head and base deployments — it records structured data points called snapshots and stores them alongside the replay. Each snapshot is tagged with:
7294
7295- \`sessionId\` — which session it was captured in, so you can align the same session on base and head. A session is essentially a recorded user flow that Meticulous replays.
7296- \`sessionDescription\` — a short, human-readable summary of what the user was doing in that session (for example \`Added an item to the cart\`), useful for labelling sessions in your report. It is \`null\` when the session has no description, so treat it as optional.
7297- \`type\` — the kind of snapshot (for example \`network-requests\`).
7298- \`stageDuringSession\` — which screenshot in the session timeline the data belongs to.
7299- \`data\` — the payload, whose shape depends on the snapshot type.
7300
7301Some snapshot types are **built in** and need no application code — Meticulous captures them automatically. Built-in types include \`network-requests\` (every \`fetch\` / XHR a session made) and \`react-component-renders\` (a per-component breakdown of which components re-rendered). You can also record your own from browser code; see [Built-in snapshot types](${o.CUSTOM_CHECKS_BUILT_IN_SNAPSHOT_TYPES_URL}) and [recording custom snapshots](${o.CUSTOM_CHECKS_RECORDING_CUSTOM_DATA_URL}).
7302
7303This check uses \`network-requests\`. Each such snapshot's \`data\` describes a single request:
7304
7305\`\`\`typescript
7306interface NetworkRequestSnapshotData {
7307  url: string;
7308  method: string;
7309  requestBody?: string;
7310  status: number | null;
7311  // ...plus request/response headers and other metadata
7312}
7313\`\`\`
7314
7315So one session that made 12 requests produces 12 \`network-requests\` snapshots, all sharing that session's \`sessionId\`.
7316
7317### Fetch the snapshots
7318
7319The SDK function for reading snapshot data is \`getSnapshotsFromTestRun(...)\`. You pass it the client, a test run id, and the \`snapshotTypes\` you care about; it downloads every matching snapshot for both the head run and its base and returns them as two arrays, \`baseSnapshots\` and \`headSnapshots\`. This is the entry point for *any* check, whatever snapshot type it reads.
7320
7321Add the snapshot type constant and a \`computeNetworkRequestsCheck\` function near the top of \`report.ts\`, above the resolving code you added in *Resolve a test run*. For now it just fetches and logs how much data we got:
7322
7323\`\`\`typescript
7324const NETWORK_REQUESTS_SNAPSHOT_TYPE = "network-requests";
7325
7326const computeNetworkRequestsCheck = async (
7327  client: MeticulousClient,
7328  testRunId: string,
7329): Promise<void> => {
7330  const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7331    client,
7332    testRunId,
7333    snapshotTypes: [NETWORK_REQUESTS_SNAPSHOT_TYPE],
7334  });
7335
7336  console.log(
7337    \`Fetched \${baseSnapshots.length} base and \${headSnapshots.length} head snapshots.\`,
7338  );
7339};
7340\`\`\`
7341
7342Then call it after resolving the run:
7343
7344\`\`\`typescript
7345await computeNetworkRequestsCheck(client, testRun.id);
7346\`\`\`
7347
7348Re-run the script — you should see non-zero counts. For large test runs this download can take some time, since one snapshot is written per captured request across every session on both base and head. We will change the return type to a check result once we have something to report.
7349
7350## 2. Compute the result of the check
7351
7352### Define the capacity model
7353
7354We now know the raw shape of the data. Next, capture the rules of the check as types and constants so the comparison logic stays readable. Add these near the top of \`report.ts\`:
7355
7356\`\`\`typescript
7357/** Stable id shown in the Meticulous UI. */
7358const CHECK_ID = "network-requests";
7359
7360/** Surface a non-blocking warning once a session exceeds base capacity by this much. */
7361const WARN_PERCENT_INCREASE_THRESHOLD = 10;
7362/** Require reviewer acknowledgement once a session exceeds base capacity by this much. */
7363const REQUIRE_ACK_PERCENT_INCREASE_THRESHOLD = 20;
7364
7365/** Ignore low-traffic sessions where +1 request is noise. */
7366const MIN_REQUESTS_FOR_ALARM = 3;
7367
7368interface EndpointComparison {
7369  label: string;
7370  baseCount: number;
7371  headCount: number;
7372  delta: number;
7373}
7374
7375interface SessionComparison {
7376  sessionId: string;
7377  // Short description of what the user did in the session (e.g. "Added an item
7378  // to the cart"), used to label the session in the report. \`null\` when the
7379  // session has no description.
7380  sessionDescription: string | null;
7381  baseCount: number;
7382  headCount: number;
7383  delta: number;
7384  percentIncrease: number;
7385  endpoints: EndpointComparison[];
7386}
7387\`\`\`
7388
7389A custom check reports one of three verdicts, typed by the SDK as \`CustomCheckVerdict\`:
7390
7391- \`pass\` — no regression; the check is green and no report is surfaced.
7392- \`warn-without-requiring-user-ack\` — surfaces a report in the Meticulous UI as a signal for reviewers to glance at, but does **not** block the pull request or require anyone to act on it.
7393- \`warn-and-require-user-ack\` — surfaces a report that a reviewer must explicitly acknowledge (review) in the Meticulous UI before the run is considered actioned, the same way an unreviewed visual diff blocks until someone accepts or ignores it.
7394
7395Note there is no \`fail\` verdict: instead of failing outright, a check escalates by *requiring acknowledgement*. (A check that errors while *running* is a separate, run-level concern, not a verdict.)
7396
7397That distinction drives the two thresholds here. We surface a non-blocking warning once a session exceeds its base capacity by 10%, and require acknowledgement at 20% — reserving the acknowledgement-required verdict for clear, actionable regressions so a single retried API call does not gate a PR. \`SessionComparison\` and \`EndpointComparison\` are the per-session and per-endpoint breakdown we will produce next.
7398
7399### Filter out noise
7400
7401Not every HTTP request reflects your application's behaviour. Analytics beacons, error trackers, font CDNs, and the Meticulous recorder itself all issue requests during replay. Counting them would let a session "regress" on telemetry noise rather than on product traffic.
7402
7403Each downloaded snapshot is a \`Snapshot\` — the SDK type for the data points from *Understand snapshots*, carrying \`sessionId\`, \`sessionDescription\`, \`type\`, \`stageDuringSession\`, and a \`data\` field typed as \`unknown\` (the SDK does not know the shape of every snapshot type). So declare the minimal shape you need and decide which requests are meaningful:
7404
7405\`\`\`typescript
7406interface NetworkRequestData {
7407  url?: string;
7408  method?: string;
7409  requestBody?: string;
7410}
7411
7412const networkRequestData = (snapshot: Snapshot): NetworkRequestData =>
7413  (snapshot.data ?? {}) as NetworkRequestData;
7414
7415/** Hostname substrings to exclude from the comparison. */
7416const IGNORED_REQUEST_HOST_SUBSTRINGS = [
7417  "sentry.io",
7418  "segment.io",
7419  "google-analytics.com",
7420  "googletagmanager.com",
7421  // Add the third-party hosts your app loads during replay.
7422];
7423
7424const isMeaningfulRequest = (snapshot: Snapshot): boolean => {
7425  const { url } = networkRequestData(snapshot);
7426  if (!url) {
7427    return true;
7428  }
7429  try {
7430    const hostname = new URL(url).hostname.toLowerCase();
7431    return !IGNORED_REQUEST_HOST_SUBSTRINGS.some((needle) =>
7432      hostname.includes(needle),
7433    );
7434  } catch {
7435    // Relative URLs (e.g. "/api/graphql") are same-origin app traffic.
7436    return true;
7437  }
7438};
7439\`\`\`
7440
7441Tailor \`IGNORED_REQUEST_HOST_SUBSTRINGS\` to your stack. Same-origin and relative URLs should always count as meaningful.
7442
7443### Count requests per session
7444
7445To explain *which* endpoints regressed, group meaningful requests by a human-readable label, then bucket them into per-session, per-endpoint counts:
7446
7447\`\`\`typescript
7448/** GraphQL calls by operation name; everything else as \`METHOD /path\`. */
7449const describeRequest = (snapshot: Snapshot): string => {
7450  const { url, method } = networkRequestData(snapshot);
7451  if (!url) {
7452    return "(unknown request)";
7453  }
7454  const verb = (method ?? "GET").toUpperCase();
7455  try {
7456    const { pathname } = new URL(url);
7457    return \`\${verb} \${pathname}\`;
7458  } catch {
7459    return \`\${verb} \${url.split("?")[0]}\`;
7460  }
7461};
7462
7463type RequestCountsByEndpoint = Map<string, number>;
7464
7465const countRequestsBySession = (
7466  snapshots: Snapshot[],
7467): Map<string, RequestCountsByEndpoint> => {
7468  const countsBySession = new Map<string, RequestCountsByEndpoint>();
7469  for (const snapshot of snapshots) {
7470    if (snapshot.type !== NETWORK_REQUESTS_SNAPSHOT_TYPE) continue;
7471    if (!isMeaningfulRequest(snapshot)) continue;
7472
7473    let byEndpoint = countsBySession.get(snapshot.sessionId);
7474    if (!byEndpoint) {
7475      byEndpoint = new Map();
7476      countsBySession.set(snapshot.sessionId, byEndpoint);
7477    }
7478    const label = describeRequest(snapshot);
7479    byEndpoint.set(label, (byEndpoint.get(label) ?? 0) + 1);
7480  }
7481  return countsBySession;
7482};
7483\`\`\`
7484
7485### Compare head against base capacity per session
7486
7487Align sessions by \`sessionId\` and compare each session's head request count against its base capacity. Skip sessions that only ran on head — they have no baseline capacity to compare against:
7488
7489\`\`\`typescript
7490const sumCounts = (byEndpoint: RequestCountsByEndpoint): number =>
7491  [...byEndpoint.values()].reduce((total, count) => total + count, 0);
7492
7493const compareEndpoints = (
7494  base: RequestCountsByEndpoint,
7495  head: RequestCountsByEndpoint,
7496): EndpointComparison[] => {
7497  const labels = new Set([...base.keys(), ...head.keys()]);
7498  return [...labels].map((label) => {
7499    const baseCount = base.get(label) ?? 0;
7500    const headCount = head.get(label) ?? 0;
7501    return { label, baseCount, headCount, delta: headCount - baseCount };
7502  });
7503};
7504
7505// First non-null \`sessionDescription\` seen per \`sessionId\`, so each session can
7506// be labelled by what the user did rather than by its opaque id.
7507const collectSessionDescriptions = (
7508  ...snapshotLists: Snapshot[][]
7509): Map<string, string | null> => {
7510  const descriptions = new Map<string, string | null>();
7511  for (const snapshots of snapshotLists) {
7512    for (const snapshot of snapshots) {
7513      const existing = descriptions.get(snapshot.sessionId);
7514      if (existing == null) {
7515        descriptions.set(snapshot.sessionId, snapshot.sessionDescription ?? null);
7516      }
7517    }
7518  }
7519  return descriptions;
7520};
7521
7522const compareSessions = (
7523  baseSnapshots: Snapshot[],
7524  headSnapshots: Snapshot[],
7525): SessionComparison[] => {
7526  const baseCounts = countRequestsBySession(baseSnapshots);
7527  const headCounts = countRequestsBySession(headSnapshots);
7528  const descriptions = collectSessionDescriptions(baseSnapshots, headSnapshots);
7529  const comparisons: SessionComparison[] = [];
7530
7531  for (const sessionId of headCounts.keys()) {
7532    // Only compare sessions that also ran on base.
7533    if (!baseCounts.has(sessionId)) continue;
7534
7535    const baseByEndpoint = baseCounts.get(sessionId) ?? new Map();
7536    const headByEndpoint = headCounts.get(sessionId) ?? new Map();
7537    const baseCount = sumCounts(baseByEndpoint);
7538    const headCount = sumCounts(headByEndpoint);
7539
7540    comparisons.push({
7541      sessionId,
7542      sessionDescription: descriptions.get(sessionId) ?? null,
7543      baseCount,
7544      headCount,
7545      delta: headCount - baseCount,
7546      percentIncrease:
7547        baseCount === 0
7548          ? headCount > 0
7549            ? Infinity
7550            : 0
7551          : ((headCount - baseCount) / baseCount) * 100,
7552      endpoints: compareEndpoints(baseByEndpoint, headByEndpoint),
7553    });
7554  }
7555
7556  return comparisons.sort((a, b) => b.delta - a.delta);
7557};
7558\`\`\`
7559
7560### Decide whether capacity was exceeded
7561
7562Every check must end on a single verdict, typed by the SDK as \`CustomCheckVerdict\` — the union \`"pass" | "warn-without-requiring-user-ack" | "warn-and-require-user-ack"\`. Turn the per-session comparisons into one of those values: did any session exceed its base capacity beyond the warn or acknowledgement thresholds? Use integer arithmetic on counts so boundary conditions (exactly +10% or +20%) are stable:
7563
7564\`\`\`typescript
7565const hasEnoughTraffic = (c: SessionComparison) =>
7566  Math.max(c.baseCount, c.headCount) >= MIN_REQUESTS_FOR_ALARM;
7567
7568const requiresAck = (c: SessionComparison) =>
7569  hasEnoughTraffic(c) &&
7570  c.baseCount > 0 &&
7571  c.headCount * 100 >=
7572    c.baseCount * (100 + REQUIRE_ACK_PERCENT_INCREASE_THRESHOLD);
7573
7574const isWarning = (c: SessionComparison) => {
7575  if (!hasEnoughTraffic(c) || c.delta <= 0 || requiresAck(c)) return false;
7576  if (c.baseCount === 0) return true; // new meaningful traffic on head
7577  return (
7578    c.headCount * 100 >=
7579    c.baseCount * (100 + WARN_PERCENT_INCREASE_THRESHOLD)
7580  );
7581};
7582
7583const computeVerdict = (
7584  comparisons: SessionComparison[],
7585): CustomCheckVerdict => {
7586  if (comparisons.some(requiresAck)) return "warn-and-require-user-ack";
7587  if (comparisons.some(isWarning)) return "warn-without-requiring-user-ack";
7588  return "pass";
7589};
7590\`\`\`
7591
7592### Build a report and return the result
7593
7594A verdict on its own does not tell a reviewer *why* the check failed. Meticulous renders a markdown report in the Checks tab, so build one that lists the sessions that exceeded capacity, with a per-endpoint table showing where the extra requests came from.
7595
7596Because this check compares **per session**, link each session in the report to its view in the Meticulous app. That page shows the base vs head comparison for the session — screenshots, timeline, and simulation details — so reviewers can see what changed without hunting for the session id. Label the link with the session's \`sessionDescription\` (e.g. "Added an item to the cart") so reviewers recognise the flow at a glance, falling back to \`session1\`, \`session2\`, … by position when a session has no description. The URL pattern is:
7597
7598\`\`\`
7599https://app.meticulous.ai/projects/<organization>/<project>/test-runs/<testRunId>/sessions/<sessionId>
7600\`\`\`
7601
7602\`<organization>\` and \`<project>\` are the URL slugs from your project's path, \`<testRunId>\` is the head test run you are reporting against, and \`<sessionId>\` is the session id from the snapshot data.
7603
7604\`\`\`typescript
7605const PROJECT_PATH =
7606  "https://app.meticulous.ai/projects/<organization>/<project>";
7607
7608const sessionUrl = (testRunId: string, sessionId: string): string =>
7609  \`\${PROJECT_PATH}/test-runs/\${testRunId}/sessions/\${sessionId}\`;
7610
7611const buildReport = (
7612  verdict: CustomCheckVerdict,
7613  comparisons: SessionComparison[],
7614  testRunId: string,
7615): string => {
7616  const alarming = comparisons.filter((c) => requiresAck(c) || isWarning(c));
7617  const lines = [
7618    "# Network request capacity",
7619    "",
7620    \`**Verdict:** \${verdict}\`,
7621    "",
7622  ];
7623
7624  alarming.forEach((comparison, index) => {
7625    // Prefer the session's recorded description; fall back to its position when
7626    // the session has none.
7627    const label = comparison.sessionDescription ?? \`session\${index + 1}\`;
7628    lines.push(
7629      \`## [\${label}](\${sessionUrl(testRunId, comparison.sessionId)}) — \${comparison.baseCount} → \${comparison.headCount} requests\`,
7630      "",
7631      "| Endpoint | Base | Head | Δ |",
7632      "| --- | ---: | ---: | ---: |",
7633    );
7634    for (const endpoint of comparison.endpoints.filter((e) => e.delta > 0)) {
7635      lines.push(
7636        \`| \${endpoint.label} | \${endpoint.baseCount} | \${endpoint.headCount} | +\${endpoint.delta} |\`,
7637      );
7638    }
7639    lines.push("");
7640  });
7641
7642  return lines.join("\\n");
7643};
7644\`\`\`
7645
7646A check hands its outcome back as a \`ReportedCustomCheckResult\` — the SDK shape that pairs your \`checkId\` with the \`verdict\`, a one-line \`summary\` shown inline in the UI, and a \`report\` (here \`{ type: "markdown", markdown }\`). Every check, whatever it measures, returns this same shape.
7647
7648Now finish the \`computeNetworkRequestsCheck\` function we started in *Fetch the snapshots*. Change its return type to \`Promise<ReportedCustomCheckResult>\` and, after fetching the snapshots, compute the comparisons, verdict, and report:
7649
7650\`\`\`typescript
7651const computeNetworkRequestsCheck = async (
7652  client: MeticulousClient,
7653  testRunId: string,
7654): Promise<ReportedCustomCheckResult> => {
7655  const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7656    client,
7657    testRunId,
7658    snapshotTypes: [NETWORK_REQUESTS_SNAPSHOT_TYPE],
7659  });
7660
7661  const comparisons = compareSessions(baseSnapshots, headSnapshots);
7662  const verdict = computeVerdict(comparisons);
7663
7664  return {
7665    checkId: CHECK_ID,
7666    verdict,
7667    summary:
7668      verdict === "pass"
7669        ? "No sessions exceeded their network request capacity"
7670        : \`\${comparisons.filter(requiresAck).length || comparisons.filter(isWarning).length} session(s) exceeded network request capacity\`,
7671    report: {
7672      type: "markdown",
7673      markdown: buildReport(verdict, comparisons, testRunId),
7674    },
7675  };
7676};
7677\`\`\`
7678
7679## 3. Report the result
7680
7681### Run the reporter locally
7682
7683Two more SDK functions complete the round trip:
7684
7685- \`findTestRunByCommitForCustomChecks(...)\` resolves a run from a **commit SHA** (waiting for it to finish). It is the CI-friendly counterpart to \`findTestRunForCustomChecks\`, which takes a run id — CI usually knows the commit, not the run id.
7686- \`reportCustomCheckResults(...)\` posts your computed results back to Meticulous in a single call (more on the "single call" rule below).
7687
7688Wire the bottom of \`report.ts\` to resolve the test run, compute the check, and either print the result (while iterating) or post it back to Meticulous. Accept **either** a positional test run id (handy while developing against your example run) **or** \`--commitSha\` (what CI uses). Replace the resolving block from *Resolve a test run* with:
7689
7690\`\`\`typescript
7691const dryRun = process.argv.includes("--dryRun");
7692
7693const resolveTestRun = async () => {
7694  const commitShaFlagIndex = process.argv.indexOf("--commitSha");
7695  const commitSha =
7696    commitShaFlagIndex !== -1
7697      ? process.argv[commitShaFlagIndex + 1]
7698      : undefined;
7699
7700  if (commitSha) {
7701    return findTestRunByCommitForCustomChecks({
7702      client,
7703      commitSha,
7704      // On a dry run we are only experimenting, so don't tell the backend that
7705      // results are coming for this run (see below).
7706      skipRegisteringExpectedCustomChecks: dryRun,
7707    });
7708  }
7709
7710  // First positional argument after \`pnpm run report --\`.
7711  const testRunId = process.argv
7712    .slice(2)
7713    .find((arg) => arg !== "--" && !arg.startsWith("-"));
7714  if (testRunId) {
7715    return findTestRunForCustomChecks({
7716      client,
7717      testRunId,
7718      skipRegisteringExpectedCustomChecks: dryRun,
7719    });
7720  }
7721
7722  throw new Error(
7723    "Pass a testRunId argument or --commitSha to identify the test run.",
7724  );
7725};
7726
7727const { testRun } = await resolveTestRun();
7728
7729const checks = [await computeNetworkRequestsCheck(client, testRun.id)];
7730
7731if (dryRun) {
7732  for (const check of checks) {
7733    console.log(\`\${check.checkId}: \${check.verdict}\\n\${check.report.markdown}\`);
7734  }
7735} else {
7736  await reportCustomCheckResults({
7737    client,
7738    testRunId: testRun.id,
7739    results: { status: "complete", checks },
7740  });
7741}
7742\`\`\`
7743
7744Both \`findTestRunForCustomChecks\` and \`findTestRunByCommitForCustomChecks\` **register** the run as expecting custom check results by default — that is what makes the **Checks** tab appear in the Meticulous UI and show a "waiting for checks" state until you report. On a real run that is exactly what you want. But while iterating locally with \`--dryRun\` you are not going to report anything, so registering would leave that run stuck showing a "waiting for checks" tab that never resolves.
7745
7746Pass \`skipRegisteringExpectedCustomChecks: true\` to suppress that signal during dry runs. Tying it to the \`dryRun\` flag (as above) means real runs still register and report normally, while local experiments stay invisible. Note that calling \`reportCustomCheckResults\` always marks the run regardless, so this only matters on the dry-run path that never reports.
7747
7748While building the check, keep using your example test run id:
7749
7750\`\`\`shell
7751METICULOUS_API_TOKEN=<token> pnpm run report -- <testRunId> --dryRun
7752\`\`\`
7753
7754In CI you pass the commit SHA instead (see *Set up CI* below). Read the printed verdict and markdown carefully before reporting for real — confirm that the sessions linked, thresholds, and endpoint breakdowns all make sense for the change under review. When you are satisfied, drop \`--dryRun\` to report results to Meticulous.
7755
7756If anything misbehaves, compare against the runnable version in the [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-c
7756hecks-examples) repo.
7757
7758### Report all checks in a single result
7759
7760A test run accepts custom check results **exactly once**. Every check you run must be computed and submitted together in one \`reportCustomCheckResults\` call, with all of them in the \`checks: [...]\` array.
7761
7762You **cannot** report checks separately — for example, posting the network requests result from one CI job and the React component renders result from another. A test run only accepts one set of results, so a second report is rejected.
7763
7764So when you add more checks later, compute them all in \`report.ts\` and send the complete set in a single API call — one script (or one CI step) that computes every check, then includes every result in the single \`checks\` array passed to \`reportCustomCheckResults\`.
7765
7766### Viewing results
7767
7768Open the test run in the Meticulous app and select the **Checks** tab. Each reported check shows its verdict, one-line summary, and rendered markdown report. For checks that require acknowledgement, reviewers can accept or ignore them from the UI, similar to reviewing visual diffs.
7769
7770## 4. Set up CI
7771
7772Add the reporter as a CI step **after** the step that kicks off your Meticulous test run. When resolved by commit SHA, the reporter waits for the test run to complete before computing and reporting results, so it can run right alongside the rest of your pipeline. A typical GitHub Actions step:
7773
7774\`\`\`yaml
7775- name: Report custom checks
7776  if: always()
7777  working-directory: custom-checks
7778  env:
7779    METICULOUS_API_TOKEN: \${{ secrets.METICULOUS_API_TOKEN }}
7780  run: pnpm run report -- --commitSha \${{ github.sha }}
7781\`\`\`
7782
7783{% callout_card variant="warning" title="Which commit SHA to pass on GitHub" %}
7784\`\${{ github.sha }}\` is usually **not** the tip of the PR branch. For \`pull_request\` workflows it is the temporary **merge commit** GitHub creates between your branch and the base branch — and that is the commit GitHub Actions checks out by default. Meticulous associates the test run with that SHA, so pass \`\${{ github.sha }}\` here rather than the PR head commit shown in the GitHub UI. On other CI providers, pass the commit SHA your Meticulous upload step used.
7785{% /callout_card %}
7786
7787Place this in the same workflow that triggers Meticulous tests (see [GitHub Actions setup](${o.GITHUB_ACTIONS_SETUP_URL})). The job needs the same API token you use for CI uploads.
7788
7789## Choosing a comparison granularity
7790
7791This check compares **per session**: it aligns each session's snapshots on base and head by \`sessionId\` and judges every session independently. That suits network request capacity, where each user flow has its own expected amount of traffic.
7792
7793Other checks may want a different granularity:
7794
7795- **Across the whole test run.** Aggregate the data over every replay without distinguishing sessions — for example, the set of unique endpoints called anywhere in the run — and compare the base aggregate against the head aggregate.
7796- **Per phase.** Use each snapshot's \`stageDuringSession\` to compare only the events that fired before a particular screenshot — for example, the requests made during page load versus after a specific interaction. This catches regressions localised to one part of a flow that a whole-session total would average out.
7797
7798Pick whichever granularity makes the regression you care about easiest to detect and explain.
7799`,eH=`---
7800{
7801  "title": "Recording custom snapshots"
7802}
7803---
7804
7805# {% $frontmatter.title %}
7806
7807Built-in custom checks use snapshots Meticulous captures automatically during replay (for example \`network-requests\`). When you need to check a metric Meticulous does not collect for you — CPU pressure, render timing, memory usage, or any other JSON-serializable signal — record it yourself from browser code during replay with \`window.Meticulous.replay.recordCustomSnapshot(...)\`.
7808
7809Your custom check reporter then downloads those snapshots (alongside built-in ones) with \`getSnapshotsFromTestRun\` and compares base vs head.
7810
7811## Prerequisites
7812
7813- Custom checks enabled for your project. Contact the Meticulous team to enable the custom checks.
7814
7815## The \`recordCustomSnapshot\` API
7816
7817During a replay, call \`window.Meticulous.replay.recordCustomSnapshot\` with:
7818
7819- \`snapshotType\` — a stable name for this kind of snapshot (you will request the same type in your reporter).
7820- \`data\` — a JSON-serializable payload (objects, arrays, strings, numbers, booleans, or \`null\`).
7821- \`versionNumber\` (optional) — increment when you change the shape of \`data\`; Meticulous can surface version mismatches between base and head in the UI.
7822
7823\`\`\`typescript
7824const result = window.Meticulous.replay.recordCustomSnapshot({
7825  snapshotType: "my-metric",
7826  data: { value: 42, unit: "ms" },
7827  versionNumber: 1,
7828});
7829
7830if (!result.success) {
7831  // Custom snapshotting is disabled for this project, or the snapshot was dropped
7832  // (see "When recording is a no-op" below).
7833}
7834\`\`\`
7835
7836### Snapshot type names
7837
7838Choose a \`snapshotType\` that:
7839
7840- Matches \`/^[a-z0-9-]{1,64}$/\` (lowercase letters, digits, and hyphens only).
7841- Does **not** collide with built-in Meticulous types — see [Built-in snapshot types](${o.CUSTOM_CHECKS_BUILT_IN_SNAPSHOT_TYPES_URL}) for the reserved names (\`network-requests\`, \`react-component-renders\`, \`custom-recording\`).
7842
7843Use one type per metric family — for example \`pressure-observer-cpu-read\` for CPU pressure readings, not a new type on every call.
7844
7845### When each snapshot is taken
7846
7847Every snapshot is tagged with a \`stageDuringSession\` — the screenshot in the session timeline that follows the recording (for example \`screenshot-after-event-00012\` or \`final-state\`). This lets you align snapshots with visual diffs when debugging a check.
7848
7849Call \`recordCustomSnapshot\` from:
7850
7851- Your app code at any point during replay (for example when an observer fires).
7852- A listener registered with \`addOnBeforeScreenshotListener\` — snapshots are tagged with the screenshot about to be taken.
7853- A listener registered with \`addOnReplayCompletionListener\` — snapshots are tagged with \`final-state\`.
7854
7855Listeners are useful when you want a consistent sampling point (for example, capture a metric before each comparison screenshot):
7856
7857\`\`\`typescript
7858window.Meticulous.replay.addOnBeforeScreenshotListener(({ stageDuringSession }) => {
7859  window.Meticulous.replay.recordCustomSnapshot({
7860    snapshotType: "my-metric",
7861    data: { stageDuringSession, value: measureSomething() },
7862  });
7863});
7864\`\`\`
7865
7866Listeners must finish quickly — they run on the screenshot critical path and are bounded by a real-time timeout. Prefer synchronous work or short microtasks; avoid waiting on stubbed timers.
7867
7868## When recording is a no-op
7869
7870\`recordCustomSnapshot\` returns \`{ success: false }\` (and does not throw) when:
7871
7872- Custom snapshot recording is **not enabled** for your project.
7873- The page is **not** running as a Meticulous replay (\`window.Meticulous.isRunningAsTest\` is false).
7874
7875Invalid input (bad \`snapshotType\`, non-JSON-serializable \`data\`, or \`undefined\` data) **throws** so you can catch misconfiguration during development.
7876
7877## Example: sampling JS heap memory during replay
7878
7879A good real-world use is tracking **JavaScript heap memory** so a check can warn when a change makes a flow noticeably heavier. This mirrors how we sample CPU pressure in our own app's [custom checks](${o.CUSTOM_CHECKS_WRITING_A_CUSTOM_CHECK_URL}).
7880
7881Many performance APIs are stubbed during replay, so read real values through \`window.Meticulous.replay.native\`. The example below samples \`performance.memory.usedJSHeapSize\` (Chromium) on an interval and records each reading as a \`js-heap-memory\` snapshot:
7882
7883\`\`\`typescript
7884const JS_HEAP_MEMORY_SNAPSHOT_TYPE = "js-heap-memory";
7885const SAMPLE_INTERVAL_MS = 1_000;
7886
7887const noop = () => {
7888  /* nothing was started */
7889};
7890
7891const startRecordingJsHeapMemory = (): (() => void) => {
7892  if (typeof window === "undefined") {
7893    return noop;
7894  }
7895
7896  // \`window.Meticulous\` is a discriminated union on \`isRunningAsTest\`; the
7897  // \`replay\` API only exists in the running-as-test variant.
7898  const meticulous = window.Meticulous;
7899  if (!meticulous?.isRunningAsTest) {
7900    return noop;
7901  }
7902  const { replay } = meticulous;
7903
7904  // \`native\` exposes the real (non-stubbed) performance metrics. \`memory\` is
7905  // only present on Chromium.
7906  const { memory } = replay.native.performance;
7907  if (!memory) {
7908    return noop;
7909  }
7910
7911  const record = () => {
7912    replay.recordCustomSnapshot({
7913      snapshotType: JS_HEAP_MEMORY_SNAPSHOT_TYPE,
7914      data: {
7915        usedJSHeapSize: memory.usedJSHeapSize,
7916        totalJSHeapSize: memory.totalJSHeapSize,
7917        time: replay.native.performance.now(),
7918      },
7919      versionNumber: 1,
7920    });
7921  };
7922
7923  // \`native.setInterval\` is the real wall-clock timer captured before stubbing,
7924  // so sampling runs on real time rather than the replay's frozen virtual time.
7925  record(); // initial sample
7926  const interval = replay.native.setInterval(record, SAMPLE_INTERVAL_MS);
7927
7928  return () => replay.native.clearInterval(interval);
7929};
7930\`\`\`
7931
7932Wire \`startRecordingJsHeapMemory()\` into your app bootstrap so it runs during Meticulous test replays (and is a no-op in production and in browsers without \`performance.memory\`).
7933
7934## Using recorded snapshots in a custom check
7935
7936In your CI reporter, include your \`snapshotType\` when downloading snapshots for the test run:
7937
7938\`\`\`typescript
7939const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7940  client,
7941  testRunId,
7942  snapshotTypes: ["js-heap-memory", "network-requests"],
7943});
7944\`\`\`
7945
7946Each snapshot includes \`sessionId\`, \`sessionDescription\` (a short, human-readable summary of what the user was doing in the session, or \`null\` when none exists), \`type\`, \`stageDuringSession\`, \`data\`, and optionally \`versionNumber\`. Compare base vs head per session using the same patterns as [Writing a custom check](${o.CUSTOM_CHECKS_WRITING_A_CUSTOM_CHECK_URL}).
7947`,e$=`---
7948{
7949  "title": "Built-in snapshot types"
7950}
7951---
7952
7953# {% $frontmatter.title %}
7954
7955Meticulous captures some snapshot types automatically during replay — no application code required. Your custom check reporter downloads them alongside any [snapshots you record yourself](${o.CUSTOM_CHECKS_RECORDING_CUSTOM_DATA_URL}) and compares base vs head per session.
7956
7957{% callout_card variant="info" title="Contact Meticulous to enable snapshots" %}
7958Built-in snapshots are not enabled by default. Reach out to the Meticulous team at [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) to turn on the snapshot types you need for your project.
7959{% /callout_card %}
7960
7961## Snapshot types Meticulous collects for you
7962
7963| Snapshot type | What it captures |
7964| --- | --- |
7965| \`network-requests\` | Every \`fetch\` / XHR issued during replay |
7966| \`react-component-renders\` | Per-component breakdown of which React components re-rendered and how often, sampled at each screenshot |
7967
7968These built-in types capture **data** snapshots. Meticulous also recognizes \`custom-recording\` as a reserved name — that is a **project capability flag** that enables \`recordCustomSnapshot\`, not a snapshot file you download. See [Recording custom snapshots](${o.CUSTOM_CHECKS_RECORDING_CUSTOM_DATA_URL}).
7969
7970Do not use these names for your own \`snapshotType\` values when calling \`recordCustomSnapshot\`.
7971
7972## Common snapshot shape
7973
7974Every snapshot — built-in or customer-recorded — is a JSON object with:
7975
7976- \`stageDuringSession\` — which comparison screenshot in the session timeline this entry belongs to (for example \`screenshot-after-event-00012\` or \`final-state\`). Meticulous tags each entry with the **next** screenshot taken after the captured activity, so you can group data per visual diff stage when debugging a check.
7977- \`data\` — the payload for that entry (schema depends on the snapshot type).
7978- \`versionNumber\` (optional) — only present on customer-recorded snapshots when you pass one to \`recordCu
7978stomSnapshot\`. Built-in snapshots omit this field.
7979
7980Snapshots for a test run are stored per replay under \`custom-checks-snapshots/\` — one uncompressed \`.json\` file per type (for example \`network-requests.json\`). Your reporter downloads them via \`getSnapshotsFromTestRun\` from the [\`@alwaysmeticulous/custom-checks\`](https://www.npmjs.com/package/@alwaysmeticulous/custom-checks) SDK; you do not read these files from S3 directly.
7981
7982When you call \`getSnapshotsFromTestRun\`, each returned snapshot also includes:
7983
7984- \`sessionId\` — the session the snapshot was captured in, so you can align the same session on base and head.
7985- \`type\` — the snapshot kind (for example \`network-requests\`), so you can filter by it.
7986- \`sessionDescription\` — a short, human-readable summary of what the user was doing in that session (for example \`Added an item to the cart\`), generated by Meticulous and handy for labelling sessions in your report. It is \`null\` when the session has no description (for example older sessions, or sessions that have not been selected for testing), so treat it as optional.
7987
7988\`\`\`typescript
7989const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7990  client,
7991  testRunId,
7992  snapshotTypes: ["network-requests", "react-component-renders"],
7993});
7994\`\`\`
7995
7996## \`network-requests\`
7997
7998**Snapshot type:** \`network-requests\`
7999
8000**When it is captured:** Every \`fetch\` or XHR the page issues during replay. Each request becomes one array entry, tagged with the session stage active when the request was made.
8001
8002**What each entry contains:**
8003
8004| Field | Description |
8005| --- | --- |
8006| \`url\` | Request URL |
8007| \`method\` | HTTP method |
8008| \`requestHeaders\` | Request headers (HAR shape) |
8009| \`requestBody\` | Request body, if any |
8010| \`status\` | Status code of the stubbed response served during replay, or \`null\` if the request was not matched to a recorded request |
8011| \`responseHeaders\` | Response headers (HAR shape) |
8012| \`matched\` | \`true\` if the request was matched and stubbed; \`false\` if it was left unmatched |
8013
8014Large request bodies follow the replay timeline's truncation rules: oversized bodies are ellipsized with an MD5 of the remainder so content changes remain detectable without storing the full payload.
8015
8016**Example entry:**
8017
8018\`\`\`json
8019{
8020  "stageDuringSession": "screenshot-after-event-00003",
8021  "data": {
8022    "url": "https://app.example.com/api/graphql",
8023    "method": "POST",
8024    "requestHeaders": [{ "name": "content-type", "value": "application/json" }],
8025    "requestBody": "{\\"query\\":\\"...\\"}",
8026    "status": 200,
8027    "responseHeaders": [{ "name": "content-type", "value": "application/json" }],
8028    "matched": true
8029  }
8030}
8031\`\`\`
8032
8033## \`react-component-renders\`
8034
8035**Snapshot type:** \`react-component-renders\`
8036
8037**When it is captured:** Meticulous installs a React DevTools-style hook before your app's \`react-dom\` initializes. On each React commit it walks the committed fiber tree and attributes the work to the components that actually re-rendered (React's \`PerformedWork\` flag), recording a **per-component breakdown** at each comparison screenshot. No application code required; a no-op on non-React pages.
8038
8039**What each entry contains:**
8040
8041| Field | Description |
8042| --- | --- |
8043| \`components\` | Per-component cumulative render counts by this stage, most-active first and capped to the busiest components |
8044
8045**Each \`components\` entry:**
8046
8047| Field | Description |
8048| --- | --- |
8049| \`name\` | The component's display name (e.g. \`UserMenu\`), or \`null\` when the name was minified away by your production build |
8050| \`source\` | Original source location of the component as \`<path>:<line>:<col>\` (resolved from your source maps), or \`null\` when it could not be resolved |
8051| \`commits\` | Cumulative number of commits in which this component re-rendered, by this stage |
8052
8053Use \`source\` (not \`name\`) to align components across base and head: production builds often minify names differently between builds, but the resolved source path is stable. Each entry counts one render **per instance**, so a component rendered in many places (a list row, an icon) can have a high \`commits\` value. What matters for a regression is the **delta vs base**: a component whose \`commits\` jumps on head pinpoints *which* component is responsible — for example from a missing memoization or an unstable prop or context value.
8054
8055**Example entry:**
8056
8057\`\`\`json
8058{
8059  "stageDuringSession": "screenshot-after-event-00007",
8060  "data": {
8061    "components": [
8062      { "name": "ResultRow", "source": "src/search/ResultRow.tsx:11:0", "commits": 220 },
8063      { "name": "ResultsList", "source": "src/search/ResultsList.tsx:24:0", "commits": 38 },
8064      { "name": null, "source": "node_modules/some-lib/Tooltip.js:8:0", "commits": 9 }
8065    ]
8066  }
8067}
8068\`\`\`
8069`,eq=`---
8070{
8071  "title": "Best practices"
8072}
8073---
8074
8075# {% $frontmatter.title %}
8076
8077Guidance for designing custom checks that surface real regressions without noisy false alarms. For an overview of how custom checks work, see [Custom checks](${o.CUSTOM_CHECKS_URL}). For a worked example, see [Writing a custom check](${o.CUSTOM_CHECKS_WRITING_A_CUSTOM_CHECK_URL}
8077).
8078
8079## Prefer deterministic metrics
8080
8081Start with [built-in snapshot types](${o.CUSTOM_CHECKS_BUILT_IN_SNAPSHOT_TYPES_URL}) such as \`network-requests\` and \`react-component-renders\` — they capture concrete, reproducible signals (request counts, URLs, per-component React re-render counts) that usually only change when your application behaviour changes.
8082
8083Metrics that fluctuate between replays even when nothing meaningful changed — CPU usage, wall-clock timing — are difficult to threshold reliably and tend to produce false alarms. Treat them with scepticism: at most use them as exploratory, non-blocking warnings, never as warnings that require a reviewer's acknowledgement.
8084
8085## Compare base against head
8086
8087Meticulous visual tests compare the head commit against its base and fail when they detect a visual difference. Custom checks should follow the same **diff semantics**: compare each metric on head against the same session's baseline on base, and only surface a warning or require acknowledgement when there is a meaningful difference relative to base.
8088
8089Avoid absolute thresholds that alarm whenever a metric crosses a fixed number (for example, "require acknowledgement if network requests exceed 50"). That pattern ignores whether the change is a regression — a session that always made 60 requests would fail every run even when your commit did not touch networking code. Instead, compare the delta between base and head (for example, "require acknowledgement if head makes more than 20% more requests than base for the same session").
8090
8091## Filter out sessions with very little data
8092
8093Many Meticulous sessions are short — a quick page view or a single interaction may produce only a handful of snapshots or requests. At that scale, a +1 delta or a large percentage swing is usually noise rather than a real regression (for example, 1 → 2 requests is +100%).
8094
8095Exclude session pairs below a minimum data floor before they can warn. [Writing a custom check](${o.CUSTOM_CHECKS_WRITING_A_CUSTOM_CHECK_URL}) uses \`MIN_REQUESTS_FOR_ALARM\` for this: neither base nor head must reach at least 3 meaningful requests before the check can alarm.
8096
8097## When comparing per session, skip head-only sessions
8098
8099If your check compares base vs head at the session level rather than aggregating across the whole test run, include only sessions that ran on **both** base and head. Sessions that appear only on head — for example because a new flow was added to the test pool to exercise a feature under development — have no baseline to regress against. Omit them from the verdict rather than treating new head traffic as a failure.
8100
8101## Reduce noise in what you measure
8102
8103Not every captured snapshot reflects the behaviour you care about. Filter out irrelevant data before comparing base and head so the check tracks real product changes, not background traffic.
8104
8105In a network request capacity check, count only *meaningful* requests — for example, calls to your own backend (same-origin \`/api/*\` routes) — and exclude third-party endpoints such as analytics, error tracking, font CDNs, and the Meticulous recorder. See [Writing a custom check](${o.CUSTOM_CHECKS_WRITING_A_CUSTOM_CHECK_URL}) for an example filter.
8106
8107## Require acknowledgement only on a high threshold
8108
8109Reserve the acknowledgement-required verdict (\`warn-and-require-user-ack\`) for clear, actionable regressions — for example, when head exceeds base capacity by 20% or more. Requiring acknowledgement on smaller increases creates noisy PR friction and trains reviewers to ignore the check.
8110
8111## Use non-blocking warnings for highly variable metrics
8112
8113When you do track a metric that fluctuates naturally between runs, prefer a non-blocking warning (\`warn-without-requiring-user-ack\`) over one that requires acknowledgement, so the Checks tab surfaces a signal without gating merges.
8114
8115## Make the report explain the outcome
8116
8117A verdict on its own rarely tells a reviewer *why* a check passed or failed. Include enough context in the markdown report for someone to understand and act on the result without re-running anything: which sessions triggered the verdict, the base and head values being compared, the delta and the threshold it crossed, and a breakdown of the specific endpoints, components, or metrics responsible.
8118
8119Where it helps, link directly to the relevant sessions in the Meticulous UI so a reviewer can jump straight to the replay. A clear, self-explanatory report is what lets reviewers confidently accept or address a check that requires acknowledgement.
8120
8121## Report every check in one call
8122
8123All custom check results for a test run must be submitted together in a single \`reportCustomCheckResults\` call. Do not split checks across separate CI jobs that POST independently as each finishes.
8124
8125## Test locally before reporting
8126
8127Before posting results to Meticulous, run your reporter against a real completed test run and inspect the output. Plan for a dry-run mode (for example a \`--dryRun\` flag) that executes your check logic and prints verdicts and markdown reports without calling the reporting API. 
8127Confirm the output matches your expectations — filtering, thresholds, session links, and breakdowns — before reporting for real. Once results are posted, reviewers see them in the Checks tab.
8128`,eB=`---
8129{
8130  "title": "Built-in checks"
8131}
8132---
8133
8134# {% $frontmatter.title %}
8135
8136Meticulous can detect non-visual regressions and report them before a pull request is merged.
8137When a check fails, the author of the PR has to acknowledge the result in Meticulous before the pull request can proceed.
8138Every built-in check can be configured to alert only, reporting its findings as warnings that never fail the status check, so you can adopt a check without blocking pull requests on it.
8139Available built-in checks:
8140
8141- [Accessibility](${o.BUILT_IN_CHECKS_ACCESSIBILITY_URL}): catch new accessibility violations introduced by a PR
8142- [Network requests](${o.BUILT_IN_CHECKS_NETWORK_REQUESTS_URL}): catch PRs that make your app issue meaningfully more network requests
8143- [React component renders](${o.BUILT_IN_CHECKS_REACT_COMPONENT_RENDERS_URL}): catch PRs that make your React components re-render meaningfully more
8144
8145A Meticulous admin can turn these on for your project.
8146`,eG=e=>`
8147## How can ${e} testing be enabled?
8148
8149Reach out to the Meticulous team at [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll turn it on for your project.
8150The check can optionally be enabled for a subset of users rather than the entire team.
8151`,eW=e=>`
8152## Does the check block merging?
8153
8154The ${e} check is blocking by default, but it can be configured to never block pull requests.
8155You can change this yourself in the project settings, no Meticulous admin needed.
8156`,ez=`---
8157{
8158  "title": "Accessibility"
8159}
8160---
8161
8162# {% $frontmatter.title %}
8163
8164Meticulous supports accessibility regression testing.
8165When a pull request is opened, Meticulous verifies that it does not introduce new accessibility regressions.
8166If it does, the author of the pull request is notified with a comment and a failing CI check.
8167Meticulous reports only **new** regressions, ignoring pre-existing ones.
8168
8169![The accessibility check in the Meticulous UI: a failed check reporting a new violation, with its rule, severity, the rendering component, the failing DOM element, fix guidance, and the number of user flows it was seen in](/docs/built-in-checks/accessibility-check-example.png)
8170
8171## How does detection work?
8172
8173Every time Meticulous takes a screenshot of your application, it runs an accessibility check on the rendered web page.
8174It then compares the pre-existing defects with the new ones and reports the freshly introduced regressions.
8175Meticulous scans the screens and states reached by your recorded sessions on every run, with no extra tests to write or maintain. This extends accessibility regression testing across your existing visual test coverage.
8176
8177## Which accessibility rules are tested?
8178
8179Meticulous checks your application against a curated list of accessibility rules.
8180These include text alternatives for images, accessible names for interactive elements and form fields, ARIA validity and required attributes, and a handful of high-signal document and semantic checks.
8181In your project settings under Checks, choose a WCAG version (2.0, 2.1, or 2.2) and level (A, AA, or AAA), then apply the available checks for that target.
8182AA includes A checks, and AAA includes A and AA checks. The catalog currently contains no AAA-specific checks, so AAA selects the same checks as AA.
8183Applying a preset replaces your WCAG rule selection, turning off checks outside that target, while preserving your separate best-practice choices. You can also toggle individual rules.
8184Each rule's badge shows its level and earliest WCAG version; the rule also applies to later versions.
8185
8186These presets select available checks; they do not establish WCAG compliance. Meticulous tests a curated subset of requirements on recorded screens and reports new violations against the base run. A full conformance assessment also requires manual testing.
8187A newly enabled rule starts reporting new violations once a base run has scanned that rule too.
8188
8189${eW("accessibility")}${eG("accessibility")}`;var eV=e.i(896773);let eK=`---
8190{
8191  "title": "Network requests"
8192}
8193---
8194
8195# {% $frontmatter.title %}
8196
8197Meticulous can check whether a pull request introduces a regression in terms of network traffic.
8198A change that makes your application chattier — an accidental N+1, a dropped cache, a component refetching on every render — often produces no visual difference at all: the page looks identical while quietly issuing far more traffic.
8199When network traffic grows past a threshold, Meticulous can block the pull request until the author reviews the per-endpoint breakdown and acknowledges the change.
8200This catches performance regressions that are invisible to visual testing but can increase backend load, infrastru
8200cture costs, and latency for users.
8201
8202![The network requests check in the Meticulous UI: a failed check reporting sessions that issued more network requests than on base, each with its request counts and a per-endpoint breakdown of which requests grew](/docs/built-in-checks/network-requests-check-example.png)
8203
8204## When does the check fail?
8205
8206If Meticulous finds a user flow that issues at least **${eV.DEFAULT_FAIL_PERCENT_INCREASE_THRESHOLD}% more** requests than on base, it marks the network requests status check as failed on the pull request, requiring the author to acknowledge the result in Meticulous before the pull request can proceed.
8207In case Meticulous finds a user flow that issues at least **${eV.DEFAULT_WARN_PERCENT_INCREASE_THRESHOLD}% more** requests than base, that is reported as an informational warning that leaves the check passing.
8208Both thresholds can be configured in the project settings.
8209
8210## Which requests are counted?
8211
8212Every \`fetch\` and XHR request the app issues during replay counts, except traffic to known telemetry and third-party hosts — analytics, error tracking, fonts (Segment, PostHog, Sentry, Datadog, Google Analytics, Google Fonts, and similar).
8213Those are excluded so that an extra analytics beacon can never flag your pull request; requests to your own backend always count.
8214The list of ignored hosts can be customised in the project settings.
8215
8216${eW("network requests")}${eG("network traffic")}`,{warnPercentIncreaseThreshold:eY,failPercentIncreaseThreshold:eJ}=eV.DEFAULT_REACT_COMPONENT_RENDERS_CHECK_CONFIG,eX=`---
8217{
8218  "title": "React component renders"
8219}
8220---
8221
8222# {% $frontmatter.title %}
8223
8224Meticulous checks whether a pull request introduces a regression in how many times your React components render.
8225Render regressions rarely show up visually — the page looks identical while a component quietly re-renders hundreds of extra times.
8226This check can block a pull request with this kind of regression from being merged, preventing the author from introducing a frontend performance regression.
8227
8228![The React component renders check in the Meticulous UI: a failed check reporting components that rendered more than on base, with a summary table and a per-component breakdown of base vs. head render counts](/docs/built-in-checks/react-component-renders-check-example.png)
8229
8230## When does the check fail?
8231
8232If Meticulous finds a component that renders at least **${eJ}% more** in a user flow, it marks the React component renders status check as failed on the pull request, requiring the author to acknowledge the result in Meticulous before the pull request can proceed.
8233A component that renders at least **${eY}% more** is reported as a warning that leaves the check passing.
8234The thresholds are configurable in the project settings.
8235
8236## Which components are considered?
8237
8238Only first-party components are considered, since third-party components typically provide a weaker signal.
8239
8240${eW("react component renders")}${eG("react component renders")}`,eQ=`---
8241{
8242  "title": "Enabling Meticulous to replay sessions fully authenticated"
8243}
8244---
8245
8246# {% $frontmatter.title %}
8247
8248Auth issues are some of the most common issues that you might encounter while setting up Meticulous.
8249This class of issues is easily identifiable by sessions recorded on logged-in pages which, when simulated, consistently redirect to
8250log-in pages or 401 screens.
8251
8252Note that you may not need to enable full authentication if you are only using Meticulous to test your frontend (read more [here](${o.TROUBLESHOOT_AUTH_URL})). If you
8253do wish to enable full authentication, then select the auth provider that you're using from the options below:
8254
8255{% tabs %}
8256{% tab label="Auth0" %}
8257
8258## Auth0
8259
8260[Auth0](https://auth0.com/) is a popular auth provider that is used by many web apps. There are many different integration methods with
8261Auth0, but, at a high level, there are two main patterns:
8262
8263### Is the user session managed in the browser?
8264
8265This integration pattern is most common in single page applications (SPAs). In these methods, the user session is managed in
8266the browser, and the browser is responsible for sending the session data to the backend with every request. Common SDKs used for this
8267pattern are [auth0-spa-js](https://github.com/auth0/auth0-spa-js) and [auth0-react](https://github.com/auth0/auth0-react).
8268
8269By default, Auth0 stores user session data in JavaScript memory, which Meticulous cannot access while recording sessions. When Meticulous
8270tries to simulate these sessions, Auth0 will fail to refresh the session data and then will force a redirect to the login screen.
8271
8272To fix this, you need to configure Auth0 to store user session data in \`localstorage\`. See Auth0's documentation [here](https://auth0.com/docs/libraries/auth0-single-page-app-sdk#change-storage-options)
8273for more information on how to set this configuration.
8274
8275### Is the user session managed in the backend?
8276
8277This integration pattern is most common in traditional web applications and in web applications that make heavy use of server-side rendering.
8278In these methods, the user session is managed in the backend, and the backend exposes this 
8278session to the frontend via cookies or headers.
8279Common SDKs used for this pattern are [auth0-node](https://github.com/auth0/node-auth0) and [nextjs-auth0](https://github.com/auth0/nextjs-auth0).
8280
8281By default, Auth0 stores user session data in httpOnly cookies, which Meticulous cannot access while recording sessions. When Meticulous
8282tries to replay these sessions, Meticulous will not pass a session cookie when attempting to load the initial page, so Auth0 will force
8283a redirect to the login screen.
8284
8285To fix this, you need to configure Auth0 to not use httpOnly cookies in the environments where Meticulous records sessions. This can be
8286accomplished by setting the \`AUTH0_COOKIE_HTTP_ONLY\` environment variable to false in the desired environments. See Auth0's documentation
8287[here](https://auth0.github.io/nextjs-auth0/types/config.ConfigParameters.html) for more information on how to set this configuration.
8288
8289### Is the same Auth0 client being used across the record and replay environments?
8290
8291If you have made the suggested changes from the previous sections and are still seeing auth issues, then it is possible that the Auth0
8292client is different between the record and simulation environments. Because live requests are being sent to Auth0 at simulation time
8293with session data collected at record time, the same Auth0 client must be used across record and simulation environments.
8294
8295Please standardize your Auth0 client across environments, and try recording and simulating a session again.
8296
8297{% /tab %}
8298
8299{% tab label="Other" %}
8300
8301## Other
8302
8303### Is the same auth provider client being used across the record and replay environments?
8304
8305Because live requests are often sent to auth providers at simulation time with session data collected at record time, the same auth provider
8306client must be used across record and simulation environments.
8307
8308Please standardize your auth provider client across environments, and try recording and simulating a session again.
8309
8310### Does your backend expose user session data to the browser exclusively via httpOnly cookies?
8311
8312If your backend exclusively uses httpOnly cookies to expose user session data to the browser, then Meticulous will not be able to access
8313this data at record time. This will prevent Meticulous from passing a valid session cookie when loading the initial page at simulation time.
8314
8315To fix this, please disable httpOnly cookies in the environments where Meticulous records sessions.
8316
8317{% /tab %}
8318
8319{% /tabs %}
8320
8321## Issues / questions?
8322
8323We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
8324
8325Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
8326
8327`,eZ=`---
8328{
8329  "title": "Bypassing Auth"
8330}
8331---
8332
8333# {% $frontmatter.title %}
8334
8335If you are only using Meticulous to test your frontend, then Meticulous won't actually need to authenticate with your backend: it'll automatically
8336stub all network responses. However, it may get tripped up if it gets redirected to a login page when trying to simulate a session.
8337
8338You can solve this by disabling the code that performs the redirect or auth check when running Meticulous tests in CI or against your preview URLs.
8339
8340This can be done securely by setting a secret in the 'Custom Request Headers' tab of your project's settings screen, or, if the check or redirect
8341being disabled does not have security implications (and only UX implications), or is for localhost inside CI, then you can disable it in code by checking [the window variables or
8342request headers that Meticulous sets](${o.METICULOUS_WINDOW_OBJECT_URL}).
8343
8344## Issues / questions?
8345
8346We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
8347
8348Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
8349
8350`;var e0=e.i(646846);let e1=(0,e0.recorderLoaderInstructions)({title:"Installing on Angular",appEntryPointDescription:"app entry point",appEntryPointExampleFileName:"`main.js` or `main.ts`",snippetTemplate:({constants:e,launchRecorderCode:t})=>`import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
8351import { AppModule } from './app/app.module';
8352import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8353${e}
8354async function startApp() {${t}
8355
8356    // Initialise app after the Meticulous recorder is ready, e.g.
8357    platformBrowserDynamic().bootstrapModule(AppModule)
8358        .catch(err => console.error(err));
8359}
8360
8361function isProduction() {
8362    // TODO: Update me with your production hostname
8363    return window.location.hostname.indexOf("your-production-site.com") > -1;
8364}
8365
8366startApp();
8367`}),e2=(0,e0.recorderLoaderInstructions)({title:"Installing on Vue",appEntryPointDescription:"app entry point",appEntryPointExampleFileName:"`main.js` or `main.ts`",snippetTemplate:({constants:e,launchRecorderCode:t})=>`import Vue from "vue";
8368import App from "./App.vue";
8369import router from "./router";
8370import store from "./store";
8371import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8372${e}
8373async function startApp() {${t}
8374
8375    // Initialise app after the Meticulous recorder is ready, e.g.
8376    new Vue({
8377      router,
8378      store,
8379      render: h => h(App)
8380    }).$mount("#app");
8381}
8382
8383function isProduction() {
8384    // TODO: Update me with your production hostname
8385    return window.location.hostname.indexOf("your-production-site.com") > -1;
8386}
8387
8388startApp();
8389`}),e3=`If you have any issues setting up the recorder then click [here](${p.METICULOUS_SETUP_CALENDLY_LINK}) to book a call with us.`,e5=`
8390If you have any cross-origin or sandboxed iFrames then the recorder should be added to each of these iFrames as well as the main frame. ${e3}
8391
8392${W}
8393`,e4=`---
8394{
8395  "title": "Set up session recording using an NPM dependency"
8396}
8397---
8398
8399# {% $frontmatter.title %}
8400
8401{% callout_card variant="warning" title="Script tag is the recommended installation method" %}
8402We recommend [installing the recorder via a script tag](${o.INSTALL_RECORDER_AS_SCRIPT_TAG_INSTALLATION_INSTRUCTIONS_URL}) instead. The script tag is the only way to fully guarantee that the recorder initializes before any other scripts execute, ensuring Meticulous can capture all network responses. Only use the NPM package if you cannot template your HTML to conditionally include the script tag.
8403{% /callout_card %}
8404
8405{% anchor id="${o.INSTALLATION_INSTRUCTIONS_ANCHOR}" /%}
8406
8407Please select your framework:
8408
8409{% tabs direction="grid" noTabSelectedByDefault=true %}
8410{% tab label="Angular" %}
8411${e1}
8412
8413${e5}
8414{% /tab %}
8415{% tab label="Vue" %}
8416${e2}
8417
8418${e5}
8419{% /tab %}
8420
8421{% tab label="React or any other framework" %}
8422${(0,e0.recorderLoaderInstructions)({title:"Installing on any other framework",appEntryPointDescription:"app entry point",appEntryPointExampleFileName:"`index.js` or `main.js`",snippetTemplate:e0.anyOtherFrameworkSnippet})}
8423
8424${e5}
8425{% /tab %}
8426
8427{% /tabs %}
8428`,e6=`---
8429{
8430  "title": "Ingest Existing Tests"
8431}
8432---
8433
8434# {% $frontmatter.title %}
8435
8436If you have existing tests (such as Playwright tests) you can configure Meticulous to record them. Meticulous will generally be able to build full coverage over your codebase without ingesting any existing tests -- however ingesting your existing tests can accelerate coverage gain in the first weeks of your setup.
8437
8438This can be particularly useful if existing tests are flaky, have no visual snapshots or have limited visual snapshots.
8439Meticulous captures visual snapshots at every point throughout the flow which can dramatically increase regression protection for these user journeys. Since it runs in a deterministic browser test suites that previously had high flake rates should not flake in Meticulous.
8440
8441## How to setup
8442
84431. Ensure the recorder is available on the page while your tests run - see [Recorder Installation](${o.INSTALL_RECORDER_URL}).
84442. Wait for recording data to be sent to Meticulous before the page is closed, or before a full page navigation (for example: page refresh or navigating to a different single page application). To do this, call [\`window.Meticulous.record.flush()\`](https://github.com/alwaysmeticulous/meticulous-sdk/blob/82bc03633f0c63e196ce5cf4bd5575fcf65a5bdb/packages/sdk-bundles-api/src/window-api/public-window-api.ts#L236) and wait for the returned promise to resolve before closing the page.
84453. You can validate the recording is working as intended by running your test suite, visiting the 'All Sessions' tab on your project page in the Meticulous UI and then modifying the filter on the view to uncheck 'Hide Automated Sessions'.
8446`,e7=`---
8447{
8448  "title": "Controlling the data recorded by the Meticulous recorder"
8449}
8450---
8451
8452# {% $frontmatter.title %}
8453
8454There are two main levers you have to control the data and sessions which are collected:
8455
8456 1. [Controlling when recording starts and stops](${o.ADDITIONAL_GUIDES.CONTROLLING_WHEN_RECORDING_STARTS_AND_STOPS_URL}): Meticulous provides an API to control when you start and stop recording a session. This allows you, for example, to only record for certain users, or certain users when visiting certain pages etc.
8457 2. [Custom redaction](${o.ADDITIONAL_GUIDES.REDACTION_URL}): You can provide custom logic to redact/filter data before it leaves the browser and gets sent to Meticulous.
8458`,e8="https://snippet.meticulous.ai/record/v1/network-recorder.bundle.js",e9="network-recorder.bundle.js",te="via-npm-dependency",tt="stop-recording-mid-session",ts=`---
8459{
8460  "title": "Controlling when recording starts and stops"
8461}
8462---
8463
8464# {% $frontmatter.title %}
8465
8466If you already server-side render your initial HTML, and have sufficient information when rendering the initial HTML to determine whether the
8467Meticulous recorder should record the session, then you can conditionally pre-render the Meticulous recorder script tag into your initial HTML.
8468In this case you can stop reading here.
8469
8470If however you need to make frontend web requests to determine whether to start recording (for example fetching user data from an API), then you
8471can use [${e9}](${e8}).
8472
8473Meticulous needs to be able to record all network requests & responses from the very
8474start of your page load for a session to replay correctly. That means that if you only want to record sessions for certain users with
8475certain attributes then you have an issue: you need to wait for the user information to load before you know whether you can enable the
8476recorder, but if you enable the recorder after the user information has loaded then the recorder won't be able to capture the initial
8477request & response to load the user information, or other early network responses.
8478
8479[${e9}](${e8}) solves this: you include it in the HTML originally returned from the server for all sessions,
8480 and it'll temporarily record any network requests in memory (but _not_ send them to the server).
8481
8482If when you load the user data you find out
8483 you _don't_ want to record the session then you can call the [\`stopIntercepting()\`](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/recorder-loader/src/early-network-recorder.ts) method from
8484 \`@alwaysmeticulous/recorder-loader\`, and any recorded data will be discarded.
8485
8486 If when you load the user
8487data you find out you _do_ want to record the session then you can call the [\`tryLoadAndStartRecorder()\`](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/recorder-loader/src/loader.ts)
8488from \`@alwaysmeticulous/recorder-loader\`, at which point the data will start getting sent to the Meticulous servers. You can also
8489[stop recording part way through a session](#${tt}), whichever way you have installed the recorder.
8490
8491{% anchor id="via-script-tag" /%}
8492## Setting up conditional recording using ${e9}
8493
8494### Step 1: Add the ${e9} script tag
8495
8496${G({isNextJs:"maybe",notPossibleToMeetRequirementsText:`If it's not possible to meet these requirements then you can [use an NPM dependency instead of a script tag](#${te}).`})}
8497
8498Add the [${e9}](${e8}) script tag to your index.html, or the HTML returned from your server:
8499
8500\`\`\`html
8501<head>
8502  ...
8503  <script
8504    src="${e8}">
8505  </script>
8506
8507  <!-- ${e9} should be added before your app -->
8508  ...
8509  <script src="main_app.js"></script>
8510</head>
8511\`\`\`
8512
8513
8514### Step 2: Conditionally start recording
8515
8516Load the data you need to determine whether to start recording. If you wish to record the session and start sending data to Meticulous then
8517call \`tryLoadAndStartRecorder()\`, otherwise call \`stopIntercepting()\`:
8518
8519{% code_with_project_selector %}
8520\`\`\`typescript
8521import { tryLoadAndStartRecorder, stopIntercepting } from "@alwaysmeticulous/recorder-loader";
8522
8523...
8524
8525const user = await loadUser();
8526if (isNotProduction() && shouldRecord(user)) {
8527  // Note: all errors are caught and logged, so no need to surround with try/catch
8528  await tryLoadAndStartRecorder({
8529    recordingToken: '{% project_recording_token /%}',
8530    isProduction: false,
8531  });
8532} else {
8533  await stopIntercepting();
8534}
8535\`\`\`
8536{% /code_with_project_selector %}
8537
8538{% anchor id="${tt}" /%}
8539## Stop recording in the middle of the session
8540
8541Once recording has started you can stop it at any point, whether you installed the recorder as an NPM package or as a script
8542tag. This is useful if you only find out part way through a session that you don't want to record it, for example because the
8543user has navigated into an area of your app that you'd rather not capture.
8544
8545{% callout_card showIcon=false %}
8546**Stopping recording is permanent for the current page load**
8547
8548Recording cannot be restarted after it has been stopped, unless the page is reloaded. Everything already uploaded is kept, and
8549the session is flagged in Meticulous as having been stopped by your application. Anything recorded since the last upload is
8550discarded, and no further data is sent to Meticulous's servers.
8551{% /callout_card %}
8552
8553{% tabs %}
8554{% tab label="NPM package" %}
8555\`tryLoadAndStartRecorder()\` resolves to a recorder object with a \`stopRecording()\` method on it. Hold onto that object so that
8556you can stop recording later on:
8557
8558{% code_with_project_selector %}
8559\`\`\`typescript
8560import { tryLoadAndStartRecorder } from "@alwaysmeticulous/recorder-loader";
8561
8562// Start the Meticulous recorder before you initialise your app.
8563// Note: all errors are caught and logged, so no need to surround with try/catch
8564const recorder = await tryLoadAndStartRecorder({
8565  recordingToken: '{% project_recording_token /%}',
8566  isProduction: false,
8567});
8568
8569// ...then later, at any point during the session:
8570await recorder.stopRecording();
8571\`\`\`
8572{% /code_with_project_selector %}
8573
8574If you are using \`tryInstallMeticulousIntercepts()\` instead then call the \`stopRecording()\` method returned by
8575\`startRecordingSession()\` in the same way.
8576{% /tab %}
8577{% tab label="Script tag" %}
8578There is no recorder object to hold onto when using a script tag, so instead call \`stopRecording()\` on the
8579\`window.Meticulous\` API that the recorder script sets up when it initialises:
8580
8581\`\`\`typescript
8582window.Meticulous?.record?.stopRecording();
8583\`\`\`
8584
8585Guard the call as above: \`window.Meticulous\` is not defined if the recorder script failed to load, or if recording was
8586disabled for this page load (for example by setting \`window.METICULOUS_DISABLED\`, or by adding a
8587\`?_meticulousDisabled=true\` query parameter to the URL). \`record\` is likewise absent while Meticulous is replaying the
8588session as a test, when there is nothing to stop -- in TypeScript, narrow on \`isRunningAsTest\` to reach it:
8589
8590\`\`\`typescript
8591if (window.Meticulous && !window.Meticulous.isRunningAsTest) {
8592  window.Meticulous.record.stopRecording();
8593}
8594\`\`\`
8595{% /tab %}
8596{% /tabs %}
8597
8598{% anchor id="${te}" /%}
8599## Alternative: using an NPM dependency
8600
8601If you prefer to use an NPM dependency, rather than a script tag, you can instead use the [tryInstallMeticulousIntercepts() function](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/recorder-loader/src/install-meticulous-intercepts.ts#L18
8602), instead of [${e9}](${e8}). However this is not recommended since it's
8603 easy to miss network requests if libraries you use snapshot references to \`window.fetch\` or \`window.XMLHttpRequest\` early in the page
8604 lifecycle ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})).
8605
8606If when you load the user data you find out you _don't_ want to re
8606cord the session then you can call the
8607\`stopRecording()\` method returned by \`tryInstallMeticulousIntercepts()\`, and any recorded data will be discarded. If when you load the user
8608data you find out you _do_ want to record the session then you can call the \`startRecordingSession()\` method returned
8609by \`tryInstallMeticulousIntercepts()\`, at which point the data will start getting sent to the Meticulous servers. You can then stop
8610recording a session at any point by calling the \`stopRecording()\` method returned by \`startRecordingSession()\`.
8611
8612 Note: \`tryInstallMeticulousIntercepts\` will return a successful promise even if for some reason the browser is unable
8613 to load the required scripts, so it's safe, and indeed [required](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL}), to block your app loading on the promise returned by \`tryInstallMeticulousIntercepts\` resolving.
8614`,to=`---
8615{
8616  "title": "Redacting/filtering data before it leaves the browser"
8617}
8618---
8619
8620# {% $frontmatter.title %}
8621
8622By default Meticulous will redact any data entered into a password field from both the user input recorded (keystrokes etc.) and the value
8623from the network requests and responses recorded. You can add additional redaction rules by passing in middleware when initializing the
8624Meticulous recorder. These can be used via a [library of helper functions](https://github.com/alwaysmeticulous/meticulous-sdk/tree/main/packages/redaction)
8625we provide for common cases, for example:
8626
8627\`\`\`typescript
8628import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8629import { dropRequestHeader, transformJsonResponse, redactRecursively, asterixOut } from "@alwaysmeticulous/redaction";
8630
8631...
8632
8633const middleware = [
8634  dropRequestHeader("Authorization"),
8635  transformJsonResponse({
8636    urlRegExp: /https:\\/\\/api\\.example\\.com\\/sensitive.*/,
8637    transform: (data) => redactRecursively(data, {
8638      redactString: str => asterixOut(str),
8639    }),
8640  }),
8641];
8642
8643await tryLoadAndStartRecorder({
8644  recordingToken: '<your recording token>',
8645  middleware
8646});
8647\`\`\`
8648
8649Or directly by writing custom transformation functions:
8650
8651\`\`\`typescript
8652import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8653
8654...
8655
8656await tryLoadAndStartRecorder({
8657  recordingToken: '<your recording token>',
8658  middleware: [
8659    {
8660      transformNetworkResponse: (response, metadata) => {
8661        if (!metadata.request.url.endsWith("get-credit-card-details")) {
8662          return response;
8663        }
8664        return {
8665          ...response,
8666          content: {
8667            ...response.content,
8668            text: JSON.stringify({ creditCardNumber: "REDACTED" }),
8669          }
8670        };
8671      }
8672    }
8673  ]
8674})
8675\`\`\`
8676
8677The full API and documentation for the middleware is available [here](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/sdk-bundles-api/src/record/middleware.ts).
8678There are some nuances, so it's worthwhile reading the [JSDoc](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/sdk-bundles-api/src/record/middleware.ts) before implementing custom middleware.
8679
8680In addition to redacting the network requests, responses and application state, you will also need to add the \`${i.METICULOUS_REDACT_RECORDING_CLASS}\`
8681class to any elements that contain data you do not want to record. This will:
8682
8683 1. Stop Meticulous recording the data inside the element in DOM snapshots. These are used for the video replays of the recorded sessions.
8684 2. Stop Meticulous recording text inside the element to identify the elements clicked on when the user clicks on an element.
8685 3. Stop Meticulous from recording keyboard events sent to any widget inside the element.
8686
8687You can use the \`${i.METICULOUS_MASK_RECORDING_PREVIEW_CLASS}\` class to stop (1) without stopping (2) and (3).
8688
8689Please reach out to [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) before implementing custom redaction. We can help
8690make sure it is implemented in a way that still allows comprehensive test coverage of all edge cases for your codebase.
8691`,tn=`---
8692{
8693  "title": "Next.js App Router - Complete Setup Guide"
8694}
8695---
8696
8697# {% $frontmatter.title %}
8698
8699Complete guide for setting up Meticulous with Next.js applications using the App Router (Next.js 13+).
8700
8701---
8702
8703## Overview
8704
8705Next.js App Router introduces React Server Components, server actions, and streaming - all of which require special handling for automated testing. This guide covers:
8706
8707- **Recorder installation** in App Router layout
8708- **CI/CD configuration** with companion assets optimization
8709- **Server component testing** with deterministic rendering
8710- **Common patterns** for authentication, data fetching, and more
8711
8712**Prerequisites**:
8713- Next.js 13+ using App Router
8714- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
8715
8716---
8717
8718## Quick Start
8719
8720### 1. Install Recorder in Root Layout
8721
8722Add the Meticulous recorder script to your root layout **before any other scripts**.
8723
8724**File**: \`app/layout.tsx\`
8725
8726\`\`\`typescript
8727import type { Metadata } from 'next'
8728
8729export const metadata: Metadata = {
8730  title: 'Your App',
8731  description: 'Your app description',
8732}
8733
8734export default function RootLayout({
8735  children,
8736}: {
8737  children: React.ReactNode
8738}) {
8739  return (
8740    <html lang="en">
8741      <head>
8742        {/* Meticulous recorder - MUST be first script */}
8743        {/* Replace YOUR_PROJECT_ID with your project ID from the dashboard */}
8744        <script
8745          data-project-id="YOUR_PROJECT_ID"
8746          src="https://snippet.meticulous.ai/v1/meticulous.js"
8747        />
8748      </head>
8749      <body>{children}</body>
8750    </html>
8751  )
8752}
8753\`\`\`
8754
8755**Important**: The recorder must load before Next.js client-side JavaScript to capture all events.
8756
8757### 2. Configure GitHub Actions Workflow
8758
8759The recommended approach for Next.js apps is to **build a Docker image of your app and have Meticulous host it** via the \`upload-container\` action. The container only needs to live for the duration of the upload step — Meticulous runs it on its own infrastructure for the actual test run.
8760
8761**File**: \`.github/workflows/meticulous.yml\`
8762
8763\`\`\`yaml
8764${g}
8765
8766      - uses: docker/setup-buildx-action@v3
8767
8768      - name: Build Docker image
8769        uses: docker/build-push-action@v6
8770        with:
8771          context: .
8772          tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
8773          platforms: linux/amd64
8774          push: false
8775          load: true
8776
8777      - name: Run Meticulous tests
8778        uses: alwaysmeticulous/report-diffs-action/upload-container@v1
8779        with:
8780          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
8781          image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
8782          # Optional: set if your container does not respect the PORT env var
8783          container-port: 3000
8784          # Optional: extra runtime env vars for the container
8785          container-env: |
8786            NODE_ENV=production
8787\`\`\`
8788
8789Your Dockerfile should:
8790- Build for \`linux/amd64\`
8791- Run \`next start\` (or equivalent) in the foreground
8792- Listen on the \`PORT\` env var (or set \`container-port\` to match)
8793- Respond \`2xx\` to a health-check endpoint (defaults to \`GET /\`; override with \`container-health-check-endpoint\`)
8794
8795If you don't already have a Dockerfile, the [Next.js with Docker example](https://github.com/vercel/next.js/tree/canary/examples/with-docker) is a good starting point.
8796
8797### 3. Add API Token Secret
8798
87991. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
88002. Go to GitHub repo → Settings → Secrets and variables → Actions
88013. Create secret named \`METICULOUS_API_TOKEN\` with your token
8802
8803### 4. Configure Project Settings
8804
8805Visit your project settings in Meticulous dashboard:
8806
88071. Go to **Network Stubbing** section
88082. Select: **"Stub all requests, apart from requests for server components and static assets"**
88093. Save settings
8810
8811This ensures React Server Components work correctly during test replay.
8812
8813---
8814
8815## Network Stubbing for Server Components
8816
8817### How It Works
8818
8819Next.js App Router makes requests to itself for Server Components (RSC protocol). These requests look like:
8820
8821\`\`\`
8822GET /?_rsc=123abc
8823\`\`\`
8824
8825**Meticulous behavior**:
8826- **Client-side API calls**: Automatically stubbed with recorded responses
8827- **Server Component requests**: Passed through to running app (not stubbed)
8828- **Static assets**: Served directly by your app (not stubbed)
8829
8830### Why This Matters
8831
8832Server Components render on the server and stream HTML to the client. Stubbing these requests would break the App Router's streaming architecture.
8833
8834**Solution**: Configure network stubbing (step 4 above) to allow Server Component requests through while stubbing external APIs.
8835
8836---
8837
8838## Ensuring Deterministic Rendering
8839
8840{% anchor id="${o.NEXTJS_APP_ROUTER_ENSURING_DETERMINISM_ANCHOR}" /%}
8841
8842### Why Determinism Matters
8843
8844Meticulous compares screenshots from before/after your code change. If rendering is non-deterministic (produces different HTML on each run), you'll get false positive diffs.
8845
8846**Common sources of non-determinism in Server Components**:
8847- \`Math.random()\`
8848- \`Date.now()\` or \`new Date()\`
8849- \`crypto.randomUUID()\`
8850- External API calls with changing data
8851
8852---
8853
8854### Handling Math.random() in Server Components
8855
8856If you use \`Math.random()\` in Server Components, prefer a request-scoped deterministic random helper:
8857
8858**Install dependency**:
8859
8860\`\`\`bash
8861npm install seedrandom
8862\`\`\`
8863
8864**Create \`lib/random.ts\`**:
8865
8866\`\`\`typescript
8867import { headers } from 'next/headers';
8868import { alea } from 'seedrandom';
8869
8870export const createDeterministicRandom = async (): Promise<() => number> => {
8871  const requestHeaders = await headers();
8872  const isMeticulousTest = requestHeaders.get('meticulous-is-test') === '1';
8873
8874  if (!isMeticulousTest) {
8875    return Math.random;
8876  }
8877
8878  const seed =
8879    requestHeaders.get('meticulous-simulated-date') ?? 'meticulous-test';
8880  const random = alea(seed);
8881
8882  return () => random();
8883};
8884\`\`\`
8885
8886**Requirements**:
8887- \`seedrandom\` package
8888
8889**How it works**:
88901. Meticulous sets \`meticulous-is-test: 1\` header during tests
88912. If you've configured a simulated date header in your Meticulous project settings (using the Simulated Date template), it provides a deterministic seed
88923. Calls to your helper return predictable values during tests
88934. Production behavior unchanged
8894
8895---
8896
8897### Handling Timestamps in Server Components
8898
8899For time-based rendering (e.g., "Posted 5 minutes ago"), you can configure Meticulous to send a simulated date header. Go to your
8900project settings (Settings > Custom Request Headers), add a header named \`meticulous-simulated-date\`, and select the **Simulated Date**
8901template for the value. Then use it in your server code:
8902
8903\`\`\`typescript
8904import { headers } from 'next/headers'
8905
8906const getCurrentDate = async (): Promise<Date> => {
8907  const requestHeaders = await headers()
8908
8909  // If a simulated date header is configured in Meticulous project settings, use it for determinism
8910  const simulatedDate = requestHeaders.get('meticulous-simulated-date')
8911
8912  if (simulatedDate) {
8913    return new Date(Date.parse(simulatedDate))
8914  }
8915
8916  return new Date()
8917}
8918
8919// Usage in Server Component
8920export default async function PostCard() {
8921  const currentDate = await getCurrentDate()
8922  const timeSincePost = calculateTimeDiff(post.createdAt, currentDate)
8923
8924  return (
8925    <div>
8926      <p>Posted {timeSincePost} ago</p>
8927    </div>
8928  );
8929}
8930\`\`\`
8931
8932**Format**: The simulated date is in RFC 7231 format (e.g., \`"Fri, 17 May 2024 13:35:20 GMT"\`)
8933
8934---
8935
8936### Testing Determinism Locally
8937
8938Use a browser extension to simulate Meticulous headers:
8939
89401. Install [ModHeader](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj)
89412. Add headers:
8942   - \`meticulous-is-test\`: \`1\`
8943   - If you've configured a simulated date header, add it too (e.g. \`meticulous-simulated-date\`: \`Fri, 17 May 2024 13:35:20 GMT\`)
89443. Visit your page and reload multiple times
89454. Content should be identical on each reload
8946
8947---
8948
8949### Alternative: Ignore Changing Elements
8950
8951If you can't make an element deterministic, ignore it in screenshots:
8952
8953**Option 1: Add CSS class**
8954
8955\`\`\`typescript
8956<div className="meticulous-ignore">
8957  Posted {timeSincePost} ago
8958</div>
8959\`\`\`
8960
8961**Option 2: Configure in project settings**
8962
8963Go to Project Settings → Screenshots & Flakes → Add CSS selectors to ignore:
8964
8965\`\`\`
8966.timestamp
8967.relative-time
8968[data-testid="posted-time"]
8969\`\`\`
8970
8971Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
8972
8973---
8974
8975## Complete Setup Example
8976
8977### File Structure
8978
8979\`\`\`
8980your-app/
8981├── app/
8982│   ├── layout.tsx              # Recorder installation
8983│   ├── page.tsx                # Server Component
8984│   └── components/
8985│       └── client-component.tsx
8986├── instrumentation.ts          # Math.random() seeding
8987├── lib/
8988│   └── date-utils.ts           # getCurrentDate helper
8989├── .github/
8990│   └── workflows/
8991│       └── meticulous.yml      # CI/CD
8992└── package.json
8993\`\`\`
8994
8995### Example: Server Component with Deterministic Rendering
8996
8997**File**: \`lib/date-utils.ts\`
8998
8999\`\`\`typescript
9000import { headers } from 'next/headers'
9001
9002export const getCurrentDate = async (): Promise<Date> => {
9003  const requestHeaders = await headers()
9004
9005  // If you've configured a simulated date header in Meticulous project settings, use it for determinism
9006  const simulatedDate = requestHeaders.get('meticulous-simulated-date')
9007  return simulatedDate ? new Date(Date.parse(simulatedDate)) : new Date()
9008}
9009
9010export const isMeticulousTest = async (): Promise<boolean> => {
9011  const requestHeaders = await headers()
9012  return requestHeaders.get('meticulous-is-test') === '1'
9013}
9014\`\`\`
9015
9016**File**: \`app/posts/[id]/page.tsx\`
9017
9018\`\`\`typescript
9019import { getCurrentDate, isMeticulousTest } from '@/lib/date-utils'
9020import { formatDistanceToNow } from 'date-fns'
9021
9022interface Post {
9023  id: string
9024  title: string
9025  content: string
9026  createdAt: Date
9027  author: {
9028    name: string
9029    avatar: string
9030  }
9031}
9032
9033async function getPost(id: string): Promise<Post> {
9034  // This fetch is automatically stubbed by Meticulous
9035  const res = await fetch(\`https://api.example.com/posts/\${id}\`)
9036  return res.json()
9037}
9038
9039export default async function PostPage({
9040  params,
9041}: {
9042  params: Promise<{ id: string }>
9043}) {
9044  const { id } = await params
9045  const post = await getPost(id)
9046  const currentDate = await getCurrentDate()
9047
9048  // Calculate time difference using deterministic date
9049  const timeAgo = formatDistanceToNow(post.createdAt, {
9050    addSuffix: true,
9051    includeSeconds: false
9052  })
9053
9054  return (
9055    <article>
9056      <h1>{post.title}</h1>
9057
9058      <div className="author-info">
9059        <img src={post.author.avatar} alt={post.author.name} />
9060        <div>
9061          <p>{post.author.name}</p>
9062          <p className="text-gray-500">
9063            Posted {timeAgo}
9064          </p>
9065        </div>
9066      </div>
9067
9068      <div className="content">
9069        {post.content}
9070      </div>
9071
9072      {(await isMeticulousTest()) && (
9073        /* Show test indicator in tests */
9074        <div className="bg-yellow-100 p-2">Running as test</div>
9075      )}
9076    </article>
9077  )
9078}
9079\`\`\`
9080
9081---
9082
9083## Common Patterns
9084
9085### Pattern 1: Detect Test Mode in Server Components
9086
9087\`\`\`typescript
9088import { headers } from 'next/headers'
9089
9090export default async function Page() {
9091  const requestHeaders = await headers()
9092  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9093
9094  if (isTest) {
9095    // Skip expensive operations during tests
9096    // Or use mock data
9097  }
9098
9099  return <div>...</div>
9100}
9101\`\`\`
9102
9103### Pattern 2: Bypass Authentication in Tests
9104
9105\`\`\`typescript
9106import { headers } from 'next/headers'
9107import { redirect } from 'next/navigation'
9108
9109export default async function ProtectedPage() {
9110  const requestHeaders = await headers()
9111  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9112
9113  if (!isTest) {
9114    const session = await getServerSession()
9115    if (!session) {
9116      redirect('/login')
9117    }
9118  }
9119
9120  // Render protected content
9121  return <div>Protected content</div>
9122}
9123\`\`\`
9124
9125### Pattern 3: Use Mock Data for Tests
9126
9127\`\`\`typescript
9128import { headers } from 'next/headers'
9129
9130async function getData() {
9131  const requestHeaders = await headers()
9132  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9133
9134  if (isTest) {
9135    // Return deterministic test data
9136    return {
9137      id: 'test-id-123',
9138      name: 'Test User',
9139      createdAt: new Date('2024-01-01T00:00:00Z')
9140    }
9141  }
9142
9143  // Fetch real data
9144  const res = await fetch('https://api.example.com/data')
9145  return res.json()
9146}
9147\`\`\`
9148
9149### Pattern 4: Handle Feature Flags
9150
9151\`\`\`typescript
9152import { headers } from 'next/headers'
9153
9154async function getFeatureFlags() {
9155  const requestHeaders = await headers()
9156  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9157
9158  if (isTest) {
9159    // Use deterministic flags during tests
9160    return {
9161      newCheckout: true,
9162      experimentalUI: false
9163    }
9164  }
9165
9166  // Fetch real flags from feature flag service
9167  return await fetchFlags()
9168}
9169\`\`\`
9170
9171---
9172
9173## CI/CD Configuration Details
9174
9175{% callout type="info" title="The sections below apply to the cloud-compute (tunnel) path only" %}
9176If you're using \`upload-container\` (the recommended approach), Meticulous serves your app from the uploaded image — there's no tunnel and no need for companion assets. The companion-assets, custom-tunnel-options, and similar sections below only apply if you're using \`cloud-compute\`.
9177{% /callout %}
9178
9179### Why Companion Assets?
9180
9181Next.js static assets (\`/_next/static/\`) are large and numerous. Serving them through the tunnel is slow.
9182
9183**Without companion assets**:
9184\`\`\`
9185Test Duration: ~5 minutes
9186Tunnel Traffic: ~50MB per test run
9187\`\`\`
9188
9189**With companion assets**:
9190\`\`\`
9191Test Duration: ~2 minutes
9192Tunnel Traffic: ~5MB per test run
9193Performance: 60% faster
9194\`\`\`
9195
9196### Companion Assets Setup
9197
9198**Step 1: Copy static files after build**
9199
9200\`\`\`yaml
9201- name: Build Next.js app
9202  run: npm run build
9203
9204- name: Prepare companion assets
9205  run: |
9206    mkdir -p companion-assets/_next
9207    cp -r .next/static companion-assets/_next/
9208    ls -la companion-assets  # Verify files copied
9209\`\`\`
9210
9211**Step 2: Configure Meticulous action**
9212
9213\`\`\`yaml
9214- name: Run Meticulous tests
9215  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
9216  with:
9217    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9218    app-url: "http://localhost:3000"
9219    companion-assets-folder: "companion-assets"
9220    companion-assets-regex: "^/_next/static/"
9221\`\`\`
9222
9223**How it works**:
92241. Meticulous intercepts requests matching \`^/_next/static/\`
92252. Files are served from \`companion-assets/_next/static/\`
92263. Other requests go through tunnel to running app
9227
9228---
9229
9230### Environment Variables
9231
9232**Build-time variables** (prefixed with \`NEXT_PUBLIC_\`):
9233
9234\`\`\`yaml
9235- name: Build Next.js app
9236  run: npm run build
9237  env:
9238    NEXT_PUBLIC_API_URL: "http://localhost:3000/api"
9239    NODE_ENV: production
9240\`\`\`
9241
9242**Runtime variables** (server-side only):
9243
9244\`\`\`yaml
9245- name: Start app
9246  run: npm start &
9247  env:
9248    DATABASE_URL: "postgresql://..."
9249    API_SECRET: \${{ secrets.API_SECRET }}
9250\`\`\`
9251
9252---
9253
9254## Troubleshooting
9255
9256### Issue: "Failed to fetch RSC payload"
9257
9258**Symptom**: Console errors about RSC fetch failures
9259
9260**Cause**: Network stubbing is blocking Server Component requests
9261
9262**Fix**: Update project settings to allow Server Component requests (see step 4 in Quick Start)
9263
9264---
9265
9266### Issue: Timestamps Cause False Positives
9267
9268**Symptom**: Diffs showing "Posted 5 min ago" vs "Posted 6 min ago"
9269
9270**Causes**:
92711. Not using a simulated date header for server-side rendering
92722. Using \`Date.now()\` or \`new Date()\` in Server Components
9273
9274**Fix**: Configure a simulated date header in Meticulous project settings using the Simulated Date template, then use the \`getCurrentDate()\` helper (see Handling Timestamps section)
9275
9276---
9277
9278### Issue: Math.random() Produces Different Results
9279
9280**Symptom**: Random UUIDs, shuffled arrays, or randomized content causes diffs
9281
9282**Fix**: Install seedrandom and setup instrumentation.ts (see Handling Math.random() section)
9283
9284---
9285
9286### Issue: Companion Assets Not Loading
9287
9288**Symptom**: Console errors for \`/_next/static/\` files, or slow test runs
9289
9290**Checks**:
92911. Verify folder exists: \`ls -la companion-assets/_next/static\`
92922. Check files were copied: Should see CSS/JS files
92933. Verify regex pattern matches: \`^/_next/static/\` should match \`/_next/static/chunks/123.js\`
92944. Check workflow syntax: Both \`companion-assets-folder\` and \`companion-assets-regex\` required
9295
9296**Debug**:
9297\`\`\`yaml
9298- name: Debug companion assets
9299  run: |
9300    echo "Checking companion assets..."
9301    ls -la companion-assets/_next/static || echo "Directory not found"
9302    find companion-assets -type f | head -10
9303\`\`\`
9304
9305---
9306
9307### Issue: App Doesn't Start in CI
9308
9309**Symptom**: "ECONNREFUSED" or "Failed to connect to http://localhost:3000"
9310
9311**Common causes**:
93121. Build failed silently
93132. Port already in use
93143. Missing environment variables
93154. App requires database connection
9316
9317**Debug steps**:
9318
9319\`\`\`yaml
9320- name: Start app with logging
9321  run: |
9322    npm start > app.log 2>&1 &
9323    sleep 5
9324    cat app.log  # Check for startup errors
9325
9326- name: Verify app is running
9327  run: |
9328    curl http://localhost:3000 || echo "App not responding"
9329    npx wait-on http://localhost:3000 --timeout 60000
9330\`\`\`
9331
9332---
9333
9334### Issue: Authentication Blocks Tests
9335
9336**Symptom**: Tests fail because pages redirect to login
9337
9338**Solutions**:
9339
9340**Option 1: Bypass auth in tests** (recommended)
9341
9342\`\`\`typescript
9343import { headers } from 'next/headers'
9344
9345const isMeticulousTest = async () => {
9346  const requestHeaders = await headers()
9347  return requestHeaders.get('meticulous-is-test') === '1'
9348}
9349
9350export default async function ProtectedLayout({ children }) {
9351  if (!(await isMeticulousTest())) {
9352    const session = await getServerSession()
9353    if (!session) redirect('/login')
9354  }
9355
9356  return <>{children}</>
9357}
9358\`\`\`
9359
9360**Option 2: Mock authentication**
9361
9362\`\`\`typescript
9363if (isMeticulousTest()) {
9364  // Return mock session for tests
9365  return {
9366    user: { id: 'test-user', name: 'Test User' }
9367  }
9368}
9369\`\`\`
9370
9371See full guide: [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL})
9372
9373---
9374
9375## Advanced Configuration
9376
9377### Custom Tunnel Options
9378
9379If you need to proxy multiple ports or use HTTPS:
9380
9381\`\`\`yaml
9382- name: Run Meticulous tests
9383  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
9384  with:
9385    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9386    app-url: "http://localhost:3000"
9387    proxy-all-urls: true  # Proxy all domains, not just app-url
9388    companion-assets-folder: "companion-assets"
9389    companion-assets-regex: "^/_next/static/"
9390\`\`\`
9391
9392See: [Tunnel Advanced Options](${o.TUNNEL_ADVANCED_OPTIONS_URL})
9393
9394---
9395
9396### Monorepo Setup
9397
9398If your Next.js app is in a subdirectory:
9399
9400\`\`\`yaml
9401- name: Install dependencies
9402  working-directory: ./apps/frontend
9403  run: npm ci
9404
9405- name: Build app
9406  working-directory: ./apps/frontend
9407  run: npm run build
9408
9409- name: Prepare companion assets
9410  working-directory: ./apps/frontend
9411  run: |
9412    mkdir -p companion-assets/_next
9413    cp -r .next/static companion-assets/_next/
9414
9415- name: Start app
9416  working-directory: ./apps/frontend
9417  run: npm start &
9418
9419- name: Run Meticulous tests
9420  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
9421  with:
9422    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9423    app-url: "http://localhost:3000"
9424    companion-assets-folder: "./apps/frontend/companion-assets"
9425    companion-assets-regex: "^/_next/static/"
9426\`\`\`
9427
9428---
9429
9430## Testing Best Practices
9431
9432### 1. Record Real User Sessions
9433
9434Record sessions in staging or production (with appropriate privacy controls):
9435
9436\`\`\`typescript
9437// Only load recorder in staging/production
9438const shouldLoadRecorder =
9439  process.env.NEXT_PUBLIC_ENV === 'staging' ||
9440  process.env.NEXT_PUBLIC_ENV === 'production'
9441
9442export default function RootLayout({ children }) {
9443  return (
9444    <html>
9445      <head>
9446        {shouldLoadRecorder && (
9447          <script
9448            data-project-id={process.env.NEXT_PUBLIC_METICULOUS_PROJECT_ID}
9449            src="https://snippet.meticulous.ai/v1/meticulous.js"
9450          />
9451        )}
9452      </head>
9453      <body>{children}</body>
9454    </html>
9455  )
9456}
9457\`\`\`
9458
9459### 2. Curate Your Test Suite
9460
9461Review recorded sessions in the dashboard and select high-value flows:
9462- Critical user journeys (signup, checkout, etc.)
9463- High-traffic pages
9464- Recently changed features
9465- Edge cases
9466
9467### 3. Handle Dynamic Content
9468
9469For content that changes frequently (ads, recommendations, live data):
9470
9471\`\`\`typescript
9472<div className="meticulous-ignore">
9473  {/* Content that changes frequently */}
9474</div>
9475\`\`\`
9476
9477### 4. Test Locally Before CI
9478
9479Run tests locally to catch issues faster:
9480
9481\`\`\`bash
9482# Start your app
9483npm run dev
9484
9485# In another terminal, run Meticulous CLI
9486npx @alwaysmeticulous/cli simulate \\
9487  --sessionId="YOUR_SESSION_ID" \\
9488  --appUrl="http://localhost:3000"
9489\`\`\`
9490
9491---
9492
9493## Migration from Pages Router
9494
9495If you're migrating from Pages Router to App Router:
9496
94971. **Keep recorder in head**: Move from \`_document.tsx\` to \`app/layout.tsx\`
94982. **Update network stubbing**: Enable Server Component request passthrough
94993. **Update Math.random() calls**: Use \`createDeterministicRandom()\` helper in Server Components
95004. **Update date handling**: Use \`getCurrentDate()\` helper in Server Components
95015. **Test thoroughly**: Server Components behave differently than client components
9502
9503---
9504
9505## Example Repository
9506
9507The configuration above is a complete App Router setup.
9508
9509---
9510
9511## See Also
9512
9513- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}
9513) - General Meticulous setup
9514- [Network Stubbing Explanation](${o.NETWORK_STUBBING_EXPLANATION_URL}) - How API mocking works
9515- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
9516- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
9517- [Companion Assets Guide](${o.COMPANION_ASSETS_ADVANCED_URL}) - Deep dive into static asset optimization
9518
9519`,ti=`---
9520{
9521  "title": "Next.js Pages Router - Complete Setup Guide"
9522}
9523---
9524
9525# {% $frontmatter.title %}
9526
9527Complete guide for setting up Meticulous with Next.js applications using the Pages Router (Next.js 12 and earlier, or Next.js 13+ without App Router).
9528
9529---
9530
9531## Overview
9532
9533The Pages Router is the traditional Next.js routing system. This guide covers:
9534
9535- **Recorder installation** in \`_document.tsx\`
9536- **CI/CD configuration** with companion assets
9537- **Authentication handling**
9538- **Common patterns** and troubleshooting
9539
9540**Prerequisites**:
9541- Next.js application using Pages Router
9542- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
9543
9544---
9545
9546## Quick Start
9547
9548### Step 1: Install Recorder in _document.tsx
9549
9550Add the Meticulous recorder script to your custom Document component **before any other scripts**.
9551
9552**File**: \`pages/_document.tsx\`
9553
9554\`\`\`typescript
9555import { Html, Head, Main, NextScript } from 'next/document'
9556
9557export default function Document() {
9558  return (
9559    <Html lang="en">
9560      <Head>
9561        {/* Meticulous recorder - MUST be first script */}
9562        {/* Replace YOUR_PROJECT_ID with your project ID from the dashboard */}
9563        <script
9564          data-project-id="YOUR_PROJECT_ID"
9565          src="https://snippet.meticulous.ai/v1/meticulous.js"
9566        />
9567      </Head>
9568      <body>
9569        <Main />
9570        <NextScript />
9571      </body>
9572    </Html>
9573  )
9574}
9575\`\`\`
9576
9577**Important**: The recorder must load before Next.js client-side JavaScript to capture all events.
9578
9579**If you don't have _document.tsx**: Create it in \`pages/_document.tsx\` with the code above.
9580
9581---
9582
9583### Step 2: Configure GitHub Actions Workflow
9584
9585Create a workflow that builds your app and uses companion assets for optimal performance.
9586
9587**File**: \`.github/workflows/meticulous.yml\`
9588
9589\`\`\`yaml
9590${g}
9591
9592      - uses: docker/setup-buildx-action@v3
9593
9594      - name: Build Docker image
9595        uses: docker/build-push-action@v6
9596        with:
9597          context: .
9598          tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
9599          platforms: linux/amd64
9600          push: false
9601          load: true
9602
9603      - name: Run Meticulous tests
9604        uses: alwaysmeticulous/report-diffs-action/upload-container@v1
9605        with:
9606          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9607          image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
9608          # Optional: set if your container does not respect the PORT env var
9609          container-port: 3000
9610          # Optional: extra runtime env vars for the container
9611          container-env: |
9612            NODE_ENV=production
9613\`\`\`
9614
9615The recommended approach is to **build a Docker image of your Next.js app and have Meticulous host it** via the \`upload-container\` action. The container only needs to live for the duration of the upload step — Meticulous runs it on its own infrastructure for the actual test run.
9616
9617Your Dockerfile should:
9618- Build for \`linux/amd64\`
9619- Run \`next start\` (or equivalent) in the foreground
9620- Listen on the \`PORT\` env var (or set \`container-port\` to match)
9621- Respond \`2xx\` to a health-check endpoint (defaults to \`GET /\`; override with \`container-health-check-endpoint\`)
9622
9623If you don't already have a Dockerfile, the [Next.js with Docker example](https://github.com/vercel/next.js/tree/canary/examples/with-docker) is a good starting point.
9624
9625---
9626
9627### Step 3: Add API Token Secret
9628
96291. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
96302. Go to GitHub repo → Settings → Secrets and variables → Actions
96313. Create secret named \`METICULOUS_API_TOKEN\` with your token
9632
9633---
9634
9635## Companion Assets
9636
9637{% callout type="info" title="Only relevant if you're using cloud-compute" %}
9638The companion-assets pattern only applies to the \`cloud-compute\` (tunnel) workflow. If you're using the recommended \`upload-container\` action, Meticulous serves your app directly from the uploaded image and there's nothing additional to configure.
9639{% /callout %}
9640
9641### Why Use Companion Assets?
9642
9643Next.js static assets (\`/_next/static/\`) are large and numerous. Serving them through the tunnel is slow.
9644
9645**Performance improvement**:
9646- **Without companion assets**: ~5 minutes test duration
9647- **With companion assets**: ~2 minutes test duration (60% faster)
9648
9649### Setup
9650
9651Add these steps to your \`cloud-compute\` workflow:
9652
9653\`\`\`yaml
9654- name: Prepare companion assets
9655  run: |
9656    mkdir -p companion-assets/_next
9657    cp -r .next/static companion-assets/_next/
9658
9659- name: Run Meticulous tests
9660  with:
9661    companion-assets-folder: "companion-assets"
9662    companion-assets-regex: "^/_next/static/"
9663\`\`\`
9664
9665**How it works**:
96661. Build creates \`.next/static/\` with all static assets
96672. Copy \`.next/static/\` to \`companion-assets/_next/static/\`
96683. Meticulous serves these files directly, bypassing the tunnel
9669
9670Learn more: [Companion Assets Guide](${o.COMPANION_ASSETS_ADVANCED_URL})
9671
9672---
9673
9674## Common Patterns
9675
9676### Pattern 1: Detect Test Mode
9677
9678Use \`window.Meticulous.isRunningAsTest\` to detect when running as a test:
9679
9680\`\`\`typescript
9681// In any component
9682function MyComponent() {
9683  const isTest = window.Meticulous?.isRunningAsTest
9684
9685  if (isTest) {
9686    // Skip animations, use test data, etc.
9687  }
9688
9689  return <div>...</div>
9690}
9691\`\`\`
9692
9693### Pattern 2: Bypass Authentication
9694
9695\`\`\`typescript
9696// pages/_app.tsx
9697import { useEffect } from 'react'
9698import { useRouter } from 'next/router'
9699
9700function MyApp({ Component, pageProps }) {
9701  const router = useRouter()
9702
9703  useEffect(() => {
9704    if (window.Meticulous?.isRunningAsTest) {
9705      // Mock authentication for tests
9706      localStorage.setItem('auth-token', 'test-token')
9707      localStorage.setItem('user', JSON.stringify({
9708        id: 'test-user',
9709        name: 'Test User',
9710        email: '[email protected]'
9711      }))
9712    }
9713  }, [])
9714
9715  return <Component {...pageProps} />
9716}
9717\`\`\`
9718
9719### Pattern 3: Server-Side Detection
9720
9721Check for \`meticulous-is-test\` header in \`getServerSideProps\`:
9722
9723\`\`\`typescript
9724export const getServerSideProps = async (context) => {
9725  const { req } = context
9726  const isTest = req.headers['meticulous-is-test'] === '1'
9727
9728  if (isTest) {
9729    // Skip auth redirect, use test data, etc.
9730    return {
9731      props: {
9732        user: { id: 'test-user', name: 'Test User' }
9733      }
9734    }
9735  }
9736
9737  // Normal server-side logic
9738  const session = await getSession(context)
9739  if (!session) {
9740    return {
9741      redirect: {
9742        destination: '/login',
9743        permanent: false,
9744      }
9745    }
9746  }
9747
9748  return {
9749    props: {
9750      user: session.user
9751    }
9752  }
9753}
9754\`\`\`
9755
9756### Pattern 4: Handle Dynamic Timestamps
9757
9758Ignore elements with frequently changing content:
9759
9760\`\`\`typescript
9761// Add meticulous-ignore class
9762<div className="meticulous-ignore">
9763  Posted {formatDistanceToNow(post.createdAt)} ago
9764</div>
9765\`\`\`
9766
9767Or configure in project settings to ignore CSS selectors globally.
9768
9769---
9770
9771## Complete Example
9772
9773### File Structure
9774
9775\`\`\`
9776your-app/
9777├── pages/
9778│   ├── _app.tsx              # App wrapper
9779│   ├── _document.tsx         # Recorder installation
9780│   ├── index.tsx             # Home page
9781│   └── dashboard.tsx         # Protected page
9782├── lib/
9783│   └── auth.ts               # Auth utilities
9784├── .github/
9785│   └── workflows/
9786│       └── meticulous.yml    # CI/CD
9787└── package.json
9788\`\`\`
9789
9790### Example: Protected Page
9791
9792**File**: \`pages/dashboard.tsx\`
9793
9794\`\`\`typescript
9795import { GetServerSideProps } from 'next'
9796import { getSession } from 'next-auth/react'
9797
9798interface DashboardProps {
9799  user: {
9800    id: string
9801    name: string
9802    email: string
9803  }
9804}
9805
9806export const getServerSideProps: GetServerSideProps<DashboardProps> = async (context) => {
9807  const isTest = context.req.headers['meticulous-is-test'] === '1'
9808
9809  if (isTest) {
9810    // Bypass auth during tests
9811    return {
9812      props: {
9813        user: {
9814          id: 'test-user-123',
9815          name: 'Test User',
9816          email: '[email protected]'
9817        }
9818      }
9819    }
9820  }
9821
9822  // Normal auth flow
9823  const session = await getSession(context)
9824
9825  if (!session) {
9826    return {
9827      redirect: {
9828        destination: '/login?redirect=/dashboard',
9829        permanent: false,
9830      }
9831    }
9832  }
9833
9834  return {
9835    props: {
9836      user: session.user
9837    }
9838  }
9839}
9840
9841export default function Dashboard({ user }: DashboardProps) {
9842  return (
9843    <div>
9844      <h1>Welcome, {user.name}!</h1>
9845      <p>Email: {user.email}</p>
9846    </div>
9847  )
9848}
9849\`\`\`
9850
9851---
9852
9853## CI/CD Configuration Details
9854
9855### Environment Variables
9856
9857**Build-time variables** (prefixed with \`NEXT_PUBLIC_\`):
9858
9859\`\`\`yaml
9860- name: Build Next.js app
9861  run: npm run build
9862  env:
9863    NEXT_PUBLIC_API_URL: "http://localhost:3000/api"
9864    NODE_ENV: production
9865\`\`\`
9866
9867**Runtime variables** (server-side only):
9868
9869\`\`\`yaml
9870- name: Start app
9871  run: npm start &
9872  env:
9873    DATABASE_URL: "postgresql://..."
9874    API_SECRET: \${{ secrets.API_SECRET }}
9875\`\`\`
9876
9877### Custom Build Scripts
9878
9879If you have a custom build process:
9880
9881\`\`\`yaml
9882- name: Build app
9883  run: |
9884    npm run build:custom
9885    npm run postbuild:assets
9886
9887- name: Prepare companion assets
9888  run: |
9889    mkdir -p companion-assets/_next
9890    cp -r .next/static companion-assets/_next/
9891    # Copy any additional static assets
9892    cp -r public/static companion-assets/static
9893\`\`\`
9894
9895---
9896
9897## Troubleshooting
9898
9899### Issue: Recorder Not Loading
9900
9901**Symptom**: \`window.Meticulous\` is undefined
9902
9903**Checks**:
99041. Verify \`_document.tsx\` has recorder script in \`<Head>\`
99052. Check project ID is correct
99063. Check for CSP blocking (console errors)
99074. Verify script loads before other scripts
9908
9909**Fix**: Ensure recorder is in \`<Head>\`, not \`<body>\`:
9910
9911\`\`\`typescript
9912<Head>
9913  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js" />
9914  {/* Other head elements */}
9915</Head>
9916\`\`\`
9917
9918---
9919
9920### Issue: App Doesn't Start in CI
9921
9922**Symptom**: "ECONNREFUSED" or "Failed to connect to http://localhost:3000"
9923
9924**Common causes**:
99251. Build failed silently
99262. Port already in use
99273. Missing environment variables
9928
9929**Debug**:
9930
9931\`\`\`yaml
9932- name: Start app with logging
9933  run: |
9934    npm start > app.log 2>&1 &
9935    sleep 5
9936    cat app.log
9937
9938- name: Verify app is running
9939  run: |
9940    curl http://localhost:3000 || echo "App not responding"
9941    npx wait-on http://localhost:3000 --timeout 60000
9942\`\`\`
9943
9944---
9945
9946### Issue: Authentication Blocks Tests
9947
9948**Symptom**: Tests fail because pages redirect to login
9949
9950**Solution 1: Bypass auth in \`getServerSideProps\`**
9951
9952\`\`\`typescript
9953export const getServerSideProps = (context) => {
9954  const isTest = context.req.headers['meticulous-is-test'] === '1'
9955
9956  if (isTest) {
9957    return { props: { user: mockUser } }
9958  }
9959
9960  // Normal auth flow
9961}
9962\`\`\`
9963
9964**Solution 2: Mock auth in \`_app.tsx\`**
9965
9966\`\`\`typescript
9967useEffect(() => {
9968  if (window.Meticulous?.isRunningAsTest) {
9969    // Set mock auth data
9970    localStorage.setItem('token', 'test-token')
9971  }
9972}, [])
9973\`\`\`
9974
9975See full guide: [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL})
9976
9977---
9978
9979### Issue: Companion Assets Not Loading
9980
9981**Symptom**: Console errors for \`/_next/static/\` files
9982
9983**Checks**:
99841. Verify folder exists: \`ls -la companion-assets/_next/static\`
99852. Check files were copied after build
99863. Verify regex pattern: \`^/_next/static/\`
9987
9988**Fix**: Ensure build completes before copying:
9989
9990\`\`\`yaml
9991- name: Build Next.js app
9992  run: npm run build
9993
9994- name: Verify build output
9995  run: ls -la .next/static
9996
9997- name: Prepare companion assets
9998  run: |
9999    mkdir -p companion-assets/_next
10000    cp -r .next/static companion-assets/_next/
10001    ls -la companion-assets/_next/static
10002\`\`\`
10003
10004---
10005
10006### Issue: False Positive Diffs
10007
10008**Symptom**: Tests show diffs for content that hasn't changed
10009
10010**Common causes**:
100111. Timestamps: "Posted 5 min ago" vs "Posted 6 min ago"
100122. Random IDs or UUIDs
100133. Animations not completing
10014
10015**Fixes**:
10016
10017**Timestamps**: Add \`meticulous-ignore\` class
10018\`\`\`typescript
10019<span className="meticulous-ignore">
10020  Posted {timeAgo} ago
10021</span>
10022\`\`\`
10023
10024**Random IDs**: Use deterministic IDs in tests
10025\`\`\`typescript
10026const generateId = () => {
10027  if (window.Meticulous?.isRunningAsTest) {
10028    return 'test-id-12345'
10029  }
10030  return crypto.randomUUID()
10031}
10032\`\`\`
10033
10034**Animations**: Disable in tests
10035\`\`\`typescript
10036const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
10037\`\`\`
10038
10039Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
10040
10041---
10042
10043## Migration from App Router
10044
10045If you're migrating to Pages Router from App Router:
10046
100471. **Move recorder**: From \`app/layout.tsx\` to \`pages/_document.tsx\`
100482. **Update auth**: Change from \`headers()\` to \`getServerSideProps\`
100493. **Test thoroughly**: Server-side logic differs between routers
10050
10051---
10052
10053## Testing Best Practices
10054
10055### 1. Record Real User Sessions
10056
10057Record sessions in staging or production (with appropriate privacy controls):
10058
10059\`\`\`typescript
10060// Only load recorder in specific environments
10061const shouldLoadRecorder =
10062  process.env.NEXT_PUBLIC_ENV === 'staging' ||
10063  process.env.NEXT_PUBLIC_ENV === 'production'
10064\`\`\`
10065
10066### 2. Handle Dynamic Content
10067
10068For frequently changing content:
10069
10070\`\`\`typescript
10071<div className="meticulous-ignore">
10072  {/* Content that changes frequently */}
10073</div>
10074\`\`\`
10075
10076### 3. Test Locally
10077
10078Run tests locally before CI:
10079
10080\`\`\`bash
10081# Start your app
10082npm run dev
10083
10084# In another terminal
10085npx @alwaysmeticulous/cli simulate \\
10086  --sessionId="YOUR_SESSION_ID" \\
10087  --appUrl="http://localhost:3000"
10088\`\`\`
10089
10090---
10091
10092## Advanced Configuration
10093
10094### Monorepo Setup
10095
10096If your Next.js app is in a subdirectory:
10097
10098\`\`\`yaml
10099- name: Install dependencies
10100  working-directory: ./apps/frontend
10101  run: npm ci
10102
10103- name: Build app
10104  working-directory: ./apps/frontend
10105  run: npm run build
10106
10107- name: Prepare companion assets
10108  working-directory: ./apps/frontend
10109  run: |
10110    mkdir -p companion-assets/_next
10111    cp -r .next/static companion-assets/_next/
10112
10113- name: Start app
10114  working-directory: ./apps/frontend
10115  run: npm start &
10116
10117- name: Run Meticulous tests
10118  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
10119  with:
10120    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10121    app-url: "http://localhost:3000"
10122    companion-assets-folder: "./apps/frontend/companion-assets"
10123    companion-assets-regex: "^/_next/static/"
10124\`\`\`
10125
10126---
10127
10128## See Also
10129
10130- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
10131- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
10132- [Companion Assets Guide](${o.COMPANION_ASSETS_ADVANCED_URL}) - Deep dive into static asset optimization
10133- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
10134`,ta=`---
10135{
10136  "title": "React with Vite - Complete Setup Guide"
10137}
10138---
10139
10140# {% $frontmatter.title %}
10141
10142Complete guide for setting up Meticulous with React applications built with Vite.
10143
10144---
10145
10146## Overview
10147
10148Vite is a fast build tool for modern web applications. This guide covers:
10149
10150- **Recorder installation** in \`index.html\`
10151- **CI/CD configuration** with static asset upload
10152- **Authentication handling**
10153- **Common patterns** and troubleshooting
10154
10155**Prerequisites**:
10156- React application using Vite
10157- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
10158
10159---
10160
10161## Quick Start
10162
10163### Step 1: Install Recorder in index.html
10164
10165Add the Meticulous recorder script to your \`index.html\` **before any other scripts**.
10166
10167**File**: \`index.html\`
10168
10169\`\`\`html
10170<!DOCTYPE html>
10171<html lang="en">
10172  <head>
10173    <meta charset="UTF-8" />
10174    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
10175    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10176    <title>Your App Name</title>
10177
10178    <!-- Meticulous recorder - MUST be first script -->
10179    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
10180    <script
10181      data-project-id="YOUR_PROJECT_ID"
10182      src="https://snippet.meticulous.ai/v1/meticulous.js"
10183    ></script>
10184  </head>
10185  <body>
10186    <div id="root"></div>
10187    <script type="module" src="/src/main.tsx"></script>
10188  </body>
10189</html>
10190\`\`\`
10191
10192**Important**: The recorder must load before your application code to capture all events.
10193
10194---
10195
10196### Step 2: Configure GitHub Actions Workflow
10197
10198Vite builds to static files, so we use the \`upload-assets\` action instead of \`cloud-compute\`.
10199
10200**File**: \`.github/workflows/meticulous.yml\`
10201
10202\`\`\`yaml
10203${g}
10204
10205      - uses: actions/setup-node@v4
10206        with:
10207          node-version: 20
10208          cache: 'npm'
10209
10210      - name: Install dependencies
10211        run: npm ci
10212
10213      - name: Build app
10214        run: npm run build
10215        env:
10216          NODE_ENV: production
10217
10218      - name: Upload and test
10219        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10220        with:
10221          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10222          app-directory: "dist"
10223          rewrites: |
10224            [
10225              { "source": "/(.*)", "destination": "/index.html" }
10226            ]
10227\`\`\`
10228
10229---
10230
10231### Step 3: Add API Token Secret
10232
102331. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
102342. Go to GitHub repo → Settings → Secrets and variables → Actions
102353. Create secret named \`METICULOUS_API_TOKEN\` with your token
10236
10237---
10238
10239## How It Works
10240
10241### upload-assets Action
10242
10243The \`upload-assets\` action:
102441. Uploads your built static files to Meticulous
102452. Serves them on a temporary URL
102463. Runs tests against that URL
102474. Reports diffs back to your PR
10248
10249**Key differences from cloud-compute**:
10250- Simpler setup (no server needed)
10251- Faster for static sites
10252- Can't test server-side logic
10253- No backend API calls (unless mocked)
10254
10255### Rewrites Configuration
10256
10257The \`rewrites\` parameter handles client-side routing:
10258
10259\`\`\`json
10260[
10261  { "source": "/(.*)", "destination": "/index.html" }
10262]
10263\`\`\`
10264
10265This ensures all routes (\`/about\`, \`/dashboard\`, etc.) serve \`index.html\`, allowing React Router to handle routing.
10266
10267---
10268
10269## Common Patterns
10270
10271### Pattern 1: Detect Test Mode
10272
10273Use \`window.Meticulous.isRunningAsTest\` to detect when running as a test:
10274
10275\`\`\`typescript
10276// In any component
10277function MyComponent() {
10278  const isTest = window.Meticulous?.isRunningAsTest
10279
10280  if (isTest) {
10281    // Skip animations, use test data, etc.
10282  }
10283
10284  return <div>...</div>
10285}
10286\`\`\`
10287
10288### Pattern 2: Bypass Authentication
10289
10290\`\`\`typescript
10291// In App.tsx or auth provider
10292import { useEffect } from 'react'
10293
10294function App() {
10295  useEffect(() => {
10296    if (window.Meticulous?.isRunningAsTest) {
10297      // Mock authentication for tests
10298      localStorage.setItem('auth-token', 'test-token')
10299      localStorage.setItem('user', JSON.stringify({
10300        id: 'test-user',
10301        name: 'Test User',
10302        email: '[email protected]'
10303      }))
10304    }
10305  }, [])
10306
10307  return <YourApp />
10308}
10309\`\`\`
10310
10311### Pattern 3: Mock API Responses
10312
10313Since there's no backend in upload-assets mode, API calls need to be mocked:
10314
10315**Option 1: Use MSW (Mock Service Worker)**
10316
10317\`\`\`typescript
10318// src/mocks/browser.ts
10319import { setupWorker } from 'msw/browser'
10320import { handlers } from './handlers'
10321
10322export const worker = setupWorker(...handlers)
10323
10324// src/main.tsx
10325if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
10326  const { worker } = await import('./mocks/browser')
10327  await worker.start()
10328}
10329\`\`\`
10330
10331**Option 2: Use recorded custom values**
10332
10333\`\`\`typescript
10334// During recording
10335window.Meticulous?.recordCustomValues?.({
10336  apiData: await fetchFromAPI()
10337})
10338
10339// During replay
10340const data = window.Meticulous?.isRunningAsTest
10341  ? window.Meticulous.getCustomValues()?.apiData
10342  : await fetchFromAPI()
10343\`\`\`
10344
10345### Pattern 4: Handle Environment Variables
10346
10347Vite exposes environment variables prefixed with \`VITE_\`:
10348
10349\`\`\`typescript
10350const apiUrl = import.meta.env.VITE_API_URL
10351
10352// Use different URL for tests
10353const effectiveUrl = window.Meticulous?.isRunningAsTest
10354  ? 'https://api.test.example.com'
10355  : apiUrl
10356\`\`\`
10357
10358---
10359
10360## Complete Example
10361
10362### File Structure
10363
10364\`\`\`
10365your-app/
10366├── src/
10367│   ├── main.tsx              # Entry point
10368│   ├── App.tsx               # Main app component
10369│   ├── components/
10370│   ├── lib/
10371│   │   └── auth.ts           # Auth utilities
10372│   └── mocks/                # MSW mocks (optional)
10373├── index.html                # Recorder installation
10374├── vite.config.ts            # Vite configuration
10375├── .github/
10376│   └── workflows/
10377│       └── meticulous.yml    # CI/CD
10378└── package.json
10379\`\`\`
10380
10381### Example: Protected Route
10382
10383**File**: \`src/App.tsx\`
10384
10385\`\`\`typescript
10386import { useEffect, useState } from 'react'
10387import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
10388
10389function App() {
10390  const [user, setUser] = useState(null)
10391  const [loading, setLoading] = useState(true)
10392
10393  useEffect(() => {
10394    // Mock auth for tests
10395    if (window.Meticulous?.isRunningAsTest) {
10396      setUser({
10397        id: 'test-user-123',
10398        name: 'Test User',
10399        email: '[email protected]'
10400      })
10401      setLoading(false)
10402      return
10403    }
10404
10405    // Normal auth flow
10406    checkAuth().then(user => {
10407      setUser(user)
10408      setLoading(false)
10409    })
10410  }, [])
10411
10412  if (loading) {
10413    return <div>Loading...</div>
10414  }
10415
10416  return (
10417    <BrowserRouter>
10418      <Routes>
10419        <Route path="/" element={<HomePage />} />
10420        <Route
10421          path="/dashboard"
10422          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
10423        />
10424        <Route path="/login" element={<LoginPage />} />
10425      </Routes>
10426    </BrowserRouter>
10427  )
10428}
10429
10430export default App
10431\`\`\`
10432
10433---
10434
10435## CI/CD Configuration Details
10436
10437### Environment Variables
10438
10439Add build-time environment variables:
10440
10441\`\`\`yaml
10442- name: Build app
10443  run: npm run build
10444  env:
10445    VITE_API_URL: "https://api.example.com"
10446    VITE_APP_NAME: "My App"
10447    NODE_ENV: production
10448\`\`\`
10449
10450**In code**:
10451\`\`\`typescript
10452const apiUrl = import.meta.env.VITE_API_URL
10453\`\`\`
10454
10455### Custom Build Directory
10456
10457If Vite outputs to a different directory:
10458
10459\`\`\`yaml
10460- name: Upload and test
10461  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10462  with:
10463    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10464    app-directory: "build" # Change from default "dist"
10465    rewrites: |
10466      [
10467        { "source": "/(.*)", "destination": "/index.html" }
10468      ]
10469\`\`\`
10470
10471### Multiple Rewrites
10472
10473For complex routing:
10474
10475\`\`\`yaml
10476rewrites: |
10477  [
10478    { "source": "/api/(.*)", "destination": "/api/index.html" },
10479    { "source": "/(.*)", "destination": "/index.html" }
10480  ]
10481\`\`\`
10482
10483---
10484
10485## Troubleshooting
10486
10487### Issue: Recorder Not Loading
10488
10489**Symptom**: \`window.Meticulous\` is undefined
10490
10491**Checks**:
104921. Verify recorder script is in \`index.html\` \`<head>\`
104932. Check project ID is correct
104943. Check for CSP blocking (console errors)
104954. Verify script loads before \`src/main.tsx\`
10496
10497**Fix**: Ensure correct order in \`index.html\`:
10498
10499\`\`\`html
10500<head>
10501  <!-- Recorder FIRST -->
10502  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
10503
10504  <!-- Then other scripts -->
10505</head>
10506<body>
10507  <div id="root"></div>
10508  <script type="module" src="/src/main.tsx"></script>
10509</body>
10510\`\`\`
10511
10512---
10513
10514### Issue: Routes Return 404
10515
10516**Symptom**: Direct navigation to \`/about\` returns 404
10517
10518**Cause**: Missing rewrite configuration
10519
10520**Fix**: Add rewrites to workflow:
10521
10522\`\`\`yaml
10523rewrites: |
10524  [
10525    { "source": "/(.*)", "destination": "/index.html" }
10526  ]
10527\`\`\`
10528
10529---
10530
10531### Issue: API Calls Fail
10532
10533**Symptom**: API requests fail during tests
10534
10535**Cause**: No backend in upload-assets mode
10536
10537**Solutions**:
10538
10539**Option 1: Mock APIs with MSW** (recommended)
10540
10541See Pattern 3 above. Keeps you on \`upload-assets\`, which is the simplest and most reliable workflow.
10542
10543**Option 2: Switch to \`upload-container\`** (if you have a backend you want to actually run)
10544
10545Build a Docker image that runs both your frontend and backend (or just your backend, if your built frontend is what \`upload-assets\` was uploading), and switch the workflow to \`upload-container\`:
10546
10547\`\`\`yaml
10548- uses: docker/setup-buildx-action@v3
10549
10550- uses: docker/build-push-action@v6
10551  with:
10552    context: .
10553    tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
10554    platforms: linux/amd64
10555    push: false
10556    load: true
10557
10558- name: Run Meticulous tests
10559  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
10560  with:
10561    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10562    image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
10563    container-port: 5173
10564\`\`\`
10565
10566---
10567
10568### Issue: Build Fails
10569
10570**Symptom**: \`npm run build\` fails in CI
10571
10572**Common causes**:
105731. TypeScript errors
105742. Missing environment variables
105753. Linting errors treated as build errors
10576
10577**Debug**:
10578
10579\`\`\`yaml
10580- name: Build app
10581  run: npm run build
10582  env:
10583    CI: false # Treats warnings as non-blocking
10584    NODE_ENV: production
10585\`\`\`
10586
10587---
10588
10589### Issue: False Positive Diffs
10590
10591**Symptom**: Tests show diffs for content that hasn't changed
10592
10593**Common causes**:
105941. Animations not completing
105952. Random IDs or keys
105963. Timestamps
10597
10598**Fixes**:
10599
10600**Animations**: Disable in tests
10601\`\`\`typescript
10602const duration = window.Meticulous?.isRunningAsTest ? 0 : 300
10603\`\`\`
10604
10605**Random IDs**: Use deterministic values
10606\`\`\`typescript
10607const generateId = () => {
10608  if (window.Meticulous?.isRunningAsTest) {
10609    return 'test-id-12345'
10610  }
10611  return crypto.randomUUID()
10612}
10613\`\`\`
10614
10615**Timestamps**: Add \`meticulous-ignore\` class
10616\`\`\`typescript
10617<span className="meticulous-ignore">
10618  {new Date().toLocaleString()}
10619</span>
10620\`\`\`
10621
10622Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
10623
10624---
10625
10626## Testing Best Practices
10627
10628### 1. Test Locally First
10629
10630Run tests locally before CI:
10631
10632\`\`\`bash
10633# Build your app
10634npm run build
10635
10636# Serve built files
10637npx serve dist
10638
10639# In another terminal, run Meticulous
10640npx @alwaysmeticulous/cli simulate \\
10641  --sessionId="YOUR_SESSION_ID" \\
10642  --appUrl="http://localhost:3000"
10643\`\`\`
10644
10645### 2. Handle Loading States
10646
10647Ensure loading states complete:
10648
10649\`\`\`typescript
10650useEffect(() => {
10651  const fetchData = async () => {
10652    setLoading(true)
10653    const data = await getData()
10654    setData(data)
10655    setLoading(false)
10656  }
10657
10658  fetchData()
10659}, [])
10660
10661if (loading) {
10662  return <div>Loading...</div>
10663}
10664\`\`\`
10665
10666### 3. Use Skeleton Screens
10667
10668Instead of spinners:
10669
10670\`\`\`typescript
10671if (loading) {
10672  return <SkeletonCard /> // Consistent placeholder
10673}
10674\`\`\`
10675
10676---
10677
10678## Advanced Configuration
10679
10680### Monorepo Setup
10681
10682If your Vite app is in a subdirectory:
10683
10684\`\`\`yaml
10685- name: Install dependencies
10686  working-directory: ./apps/frontend
10687  run: npm ci
10688
10689- name: Build app
10690  working-directory: ./apps/frontend
10691  run: npm run build
10692
10693- name: Upload and test
10694  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10695  with:
10696    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10697    app-directory: "./apps/frontend/dist"
10698    rewrites: |
10699      [
10700        { "source": "/(.*)", "destination": "/index.html" }
10701      ]
10702\`\`\`
10703
10704### Custom Vite Config
10705
10706If you have a custom Vite config:
10707
10708**File**: \`vite.config.ts\`
10709
10710\`\`\`typescript
10711import { defineConfig } from 'vite'
10712import react from '@vitejs/plugin-react'
10713import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'
10714
10715export default defineConfig({
10716  plugins: [react(), CssSourcemapPlugin()],
10717  build: {
10718    outDir: 'dist',
10719    sourcemap: true,
10720  },
10721  server: {
10722    port: 5173,
10723  }
10724})
10725\`\`\`
10726
10727**Important**: \`build.sourcemap\` covers JavaScript only — it does nothing for CSS. Vite emits no CSS source maps for
10728production builds at all, so without extra help Meticulous cannot attribute stylesheet coverage back to the files in your repo.
10729\`@alwaysmeticulous/recorder-plugin/css-sourcemap\` emits a \`.css.map\` for each CSS asset to close that gap.
10730
10731The plugin disables Vite's CSS minification, which is what makes the maps accurate, so enable it on the build whose coverage
10732Meticulous collects rather than on every production build. See
10733[Viewing source coverage information](${o.ENABLE_SOURCE_COVERAGE_URL}) for the accuracy details and the full set of options.
10734
10735---
10736
10737## See Also
10738
10739- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
10740- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
10741- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
10742`,tr=`---
10743{
10744  "title": "Create React App - Complete Setup Guide"
10745}
10746---
10747
10748# {% $frontmatter.title %}
10749
10750Complete guide for setting up Meticulous with React applications built with Create React App (CRA).
10751
10752---
10753
10754## Overview
10755
10756Create React App is the official way to create single-page React applications. This guide covers:
10757
10758- **Recorder installation** in \`public/index.html\`
10759- **CI/CD configuration** with static asset upload
10760- **Authentication handling**
10761- **Common patterns** and troubleshooting
10762
10763**Prerequisites**:
10764- React application created with Create React App
10765- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
10766
10767**Note**: Create React App is no longer actively maintained. For new projects, consider using [Vite](${o.REACT_VITE_URL}) or Next.js.
10768
10769---
10770
10771## Quick Start
10772
10773### Step 1: Install Recorder in public/index.html
10774
10775Add the Meticulous recorder script to your \`public/index.html\` **before any other scripts**.
10776
10777**File**: \`public/index.html\`
10778
10779\`\`\`html
10780<!DOCTYPE html>
10781<html lang="en">
10782  <head>
10783    <meta charset="utf-8" />
10784    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
10785    <meta name="viewport" content="width=device-width, initial-scale=1" />
10786    <meta name="theme-color" content="#000000" />
10787    <meta name="description" content="Your app description" />
10788    <title>Your App Name</title>
10789
10790    <!-- Meticulous recorder - MUST be first script -->
10791    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
10792    <script
10793      data-project-id="YOUR_PROJECT_ID"
10794      src="https://snippet.meticulous.ai/v1/meticulous.js"
10795    ></script>
10796  </head>
10797  <body>
10798    <noscript>You need to enable JavaScript to run this app.</noscript>
10799    <div id="root"></div>
10800  </body>
10801</html>
10802\`\`\`
10803
10804**Important**: The recorder must load before React to capture all events.
10805
10806---
10807
10808### Step 2: Configure GitHub Actions Workflow
10809
10810CRA builds to static files, so we use the \`upload-assets\` action.
10811
10812**File**: \`.github/workflows/meticulous.yml\`
10813
10814\`\`\`yaml
10815${g}
10816
10817      - uses: actions/setup-node@v4
10818        with:
10819          node-version: 20
10820          cache: 'npm'
10821
10822      - name: Install dependencies
10823        run: npm ci
10824
10825      - name: Build app
10826        run: npm run build
10827        env:
10828          NODE_ENV: production
10829          CI: false # Treats warnings as non-blocking
10830
10831      - name: Upload and test
10832        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10833        with:
10834          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10835          app-directory: "build"
10836          rewrites: |
10837            [
10838              { "source": "/(.*)", "destination": "/index.html" }
10839            ]
10840\`\`\`
10841
10842**Note**: \`CI: false\` prevents build from failing on warnings. Remove if you want strict builds.
10843
10844---
10845
10846### Step 3: Add API Token Secret
10847
108481. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
108492. Go to GitHub repo → Settings → Secrets and variables → Actions
108503. Create secret named \`METICULOUS_API_TOKEN\` with your token
10851
10852---
10853
10854## How It Works
10855
10856### upload-assets Action
10857
10858The \`upload-assets\` action:
108591. Uploads your built static files to Meticulous
108602. Serves them on a temporary URL
108613. Runs tests against that URL
108624. Reports diffs back to your PR
10863
10864**Build output**: CRA builds to \`build/\` directory by default.
10865
10866### Rewrites Configuration
10867
10868The \`rewrites\` parameter handles client-side routing:
10869
10870\`\`\`json
10871[
10872  { "source": "/(.*)", "destination": "/index.html" }
10873]
10874\`\`\`
10875
10876This ensures all routes serve \`index.html\`, allowing React Router to handle routing.
10877
10878---
10879
10880## Common Patterns
10881
10882### Pattern 1: Detect Test Mode
10883
10884Use \`window.Meticulous.isRunningAsTest\` in your components:
10885
10886\`\`\`typescript
10887function MyComponent() {
10888  const isTest = window.Meticulous?.isRunningAsTest
10889
10890  if (isTest) {
10891    // Skip animations, use test data, etc.
10892  }
10893
10894  return <div>...</div>
10895}
10896\`\`\`
10897
10898### Pattern 2: Bypass Authentication
10899
10900\`\`\`typescript
10901// In src/App.js or auth provider
10902import { useEffect } from 'react'
10903
10904function App() {
10905  useEffect(() => {
10906    if (window.Meticulous?.isRunningAsTest) {
10907      // Mock authentication for tests
10908      localStorage.setItem('auth-token', 'test-token')
10909      localStorage.setItem('user', JSON.stringify({
10910        id: 'test-user',
10911        name: 'Test User',
10912        email: '[email protected]'
10913      }))
10914    }
10915  }, [])
10916
10917  return <YourApp />
10918}
10919\`\`\`
10920
10921### Pattern 3: Handle Environment Variables
10922
10923CRA uses \`REACT_APP_\` prefix for environment variables:
10924
10925\`\`\`typescript
10926const apiUrl = process.env.REACT_APP_API_URL
10927
10928// Use different URL for tests
10929const effectiveUrl = window.Meticulous?.isRunningAsTest
10930  ? 'https://api.test.example.com'
10931  : apiUrl
10932\`\`\`
10933
10934**In workflow**:
10935\`\`\`yaml
10936- name: Build app
10937  run: npm run build
10938  env:
10939    REACT_APP_API_URL: "https://api.example.com"
10940    NODE_ENV: production
10941    CI: false
10942\`\`\`
10943
10944### Pattern 4: Mock Service Worker for APIs
10945
10946Use MSW to mock API calls:
10947
10948\`\`\`typescript
10949// src/mocks/browser.js
10950import { setupWorker } from 'msw/browser'
10951import { handlers } from './handlers'
10952
10953export const worker = setupWorker(...handlers)
10954
10955// src/index.js
10956if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
10957  const { worker } = await import('./mocks/browser')
10958  await worker.start()
10959}
10960
10961ReactDOM.render(<App />, document.getElementById('root'))
10962\`\`\`
10963
10964---
10965
10966## Complete Example
10967
10968### File Structure
10969
10970\`\`\`
10971your-app/
10972├── public/
10973│   └── index.html            # Recorder installation
10974├── src/
10975│   ├── index.js              # Entry point
10976│   ├── App.js                # Main app component
10977│   ├── components/
10978│   ├── lib/
10979│   │   └── auth.js           # Auth utilities
10980│   └── mocks/                # MSW mocks (optional)
10981├── .github/
10982│   └── workflows/
10983│       └── meticulous.yml    # CI/CD
10984├── package.json
10985└── .env                      # Environment variables
10986\`\`\`
10987
10988### Example: App with Authentication
10989
10990**File**: \`src/App.js\`
10991
10992\`\`\`jsx
10993import { useEffect, useState } from 'react'
10994import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
10995import { checkAuth } from './lib/auth'
10996
10997function App() {
10998  const [user, setUser] = useState(null)
10999  const [loading, setLoading] = useState(true)
11000
11001  useEffect(() => {
11002    // Mock auth for tests
11003    if (window.Meticulous?.isRunningAsTest) {
11004      setUser({
11005        id: 'test-user-123',
11006        name: 'Test User',
11007        email: '[email protected]'
11008      })
11009      setLoading(false)
11010      return
11011    }
11012
11013    // Normal auth flow
11014    checkAuth().then(user => {
11015      setUser(user)
11016      setLoading(false)
11017    }).catch(() => {
11018      setUser(null)
11019      setLoading(false)
11020    })
11021  }, [])
11022
11023  if (loading) {
11024    return (
11025      <div className="loading">
11026        <p>Loading...</p>
11027      </div>
11028    )
11029  }
11030
11031  return (
11032    <BrowserRouter>
11033      <Routes>
11034        <Route path="/" element={<HomePage />} />
11035        <Route
11036          path="/dashboard"
11037          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
11038        />
11039        <Route path="/login" element={<LoginPage />} />
11040      </Routes>
11041    </BrowserRouter>
11042  )
11043}
11044
11045export default App
11046\`\`\`
11047
11048---
11049
11050## CI/CD Configuration Details
11051
11052### Environment Variables
11053
11054CRA environment variables must be prefixed with \`REACT_APP_\`:
11055
11056\`\`\`yaml
11057- name: Build app
11058  run: npm run build
11059  env:
11060    REACT_APP_API_URL: "https://api.example.com"
11061    REACT_APP_APP_NAME: "My App"
11062    NODE_ENV: production
11063    CI: false
11064\`\`\`
11065
11066**In code**:
11067\`\`\`javascript
11068const apiUrl = process.env.REACT_APP_API_URL
11069\`\`\`
11070
11071### Custom Build Script
11072
11073If you have a custom build script:
11074
11075\`\`\`yaml
11076- name: Build app
11077  run: npm run build:custom
11078  env:
11079    NODE_ENV: production
11080\`\`\`
11081
11082### Using Yarn
11083
11084If your project uses Yarn:
11085
11086\`\`\`yaml
11087- uses: actions/setup-node@v4
11088  with:
11089    node-version: 20
11090    cache: 'yarn'
11091
11092- name: Install dependencies
11093  run: yarn install --frozen-lockfile
11094
11095- name: Build app
11096  run: yarn build
11097\`\`\`
11098
11099---
11100
11101## Troubleshooting
11102
11103### Issue: Build Fails with Warnings
11104
11105**Symptom**: Build fails in CI with ESLint or TypeScript warnings
11106
11107**Cause**: CRA treats warnings as errors when \`CI=true\`
11108
11109**Fix**: Set \`CI=false\` in build step:
11110
11111\`\`\`yaml
11112- name: Build app
11113  run: npm run build
11114  env:
11115    CI: false
11116    NODE_ENV: production
11117\`\`\`
11118
11119**Alternative**: Fix the warnings properly
11120
11121---
11122
11123### Issue: Recorder Not Loading
11124
11125**Symptom**: \`window.Meticulous\` is undefined
11126
11127**Checks**:
111281. Verify recorder script is in \`public/index.html\` \`<head>\`
111292. Check project ID is correct
111303. Check for CSP blocking (console errors)
11131
11132**Fix**: Ensure correct placement in \`public/index.html\`:
11133
11134\`\`\`html
11135<head>
11136  <!-- Recorder FIRST -->
11137  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
11138
11139  <!-- Then other head elements -->
11140  <title>Your App</title>
11141</head>
11142\`\`\`
11143
11144---
11145
11146### Issue: Routes Return 404
11147
11148**Symptom**: Direct navigation to routes like \`/about\` returns 404
11149
11150**Cause**: Missing rewrite configuration
11151
11152**Fix**: Ensure rewrites in workflow:
11153
11154\`\`\`yaml
11155rewrites: |
11156  [
11157    { "source": "/(.*)", "destination": "/index.html" }
11158  ]
11159\`\`\`
11160
11161---
11162
11163### Issue: Environment Variables Not Working
11164
11165**Symptom**: \`process.env.REACT_APP_API_URL\` is undefined
11166
11167**Common causes**:
111681. Variable not prefixed with \`REACT_APP_\`
111692. Not added to workflow \`env:\` section
111703. Trying to access in workflow but not in code
11171
11172**Fix**:
11173
11174\`\`\`yaml
11175# In workflow
11176- name: Build app
11177  run: npm run build
11178  env:
11179    REACT_APP_API_URL: "https://api.example.com" # Must have REACT_APP_ prefix
11180\`\`\`
11181
11182\`\`\`javascript
11183// In code
11184const apiUrl = process.env.REACT_APP_API_URL
11185console.log('API URL:', apiUrl) // Should log the URL
11186\`\`\`
11187
11188---
11189
11190### Issue: API Calls Fail
11191
11192**Symptom**: API requests fail during tests
11193
11194**Cause**: No backend in upload-assets mode
11195
11196**Solutions**:
11197
11198**Option 1: Mock APIs with MSW** (recommended — see Pattern 4 above)
11199
11200Keeps you on \`upload-assets\`, which is the simplest and most reliable workflow.
11201
11202**Option 2: Switch to \`upload-container\`** (if you have a backend you want to actually run)
11203
11204Build a Docker image that runs your backend (and optionally your frontend), and switch the workflow to \`upload-container\`:
11205
11206\`\`\`yaml
11207- uses: docker/setup-buildx-action@v3
11208
11209- uses: docker/build-push-action@v6
11210  with:
11211    context: .
11212    tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
11213    platforms: linux/amd64
11214    push: false
11215    load: true
11216
11217- name: Run Meticulous tests
11218  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
11219  with:
11220    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11221    image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
11222    container-port: 3000
11223\`\`\`
11224
11225---
11226
11227### Issue: False Positive Diffs
11228
11229**Symptom**: Tests show diffs for unchanged content
11230
11231**Common causes**:
112321. Animations not completing
112332. Random keys or IDs
112343. Timestamps or dynamic data
11235
11236**Fixes**:
11237
11238**Animations**: Disable in tests
11239\`\`\`javascript
11240const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
11241\`\`\`
11242
11243**Random IDs**: Use deterministic values
11244\`\`\`javascript
11245const generateId = () => {
11246  if (window.Meticulous?.isRunningAsTest) {
11247    return 'test-id-12345'
11248  }
11249  return Math.random().toString(36).substr(2, 9)
11250}
11251\`\`\`
11252
11253**Timestamps**: Add \`meticulous-ignore\` class
11254\`\`\`jsx
11255<span className="meticulous-ignore">
11256  Last updated: {new Date().toLocaleString()}
11257</span>
11258\`\`\`
11259
11260Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
11261
11262---
11263
11264## Testing Best Practices
11265
11266### 1. Test Build Locally
11267
11268Before pushing to CI:
11269
11270\`\`\`bash
11271# Build your app
11272npm run build
11273
11274# Serve built files
11275npx serve -s build
11276
11277# Verify it works at http://localhost:3000
11278\`\`\`
11279
11280### 2. Handle Loading States Properly
11281
11282Ensure loading states complete before rendering content:
11283
11284\`\`\`jsx
11285if (loading) {
11286  return <div className="loading">Loading...</div>
11287}
11288
11289if (error) {
11290  return <div className="error">Error: {error.message}</div>
11291}
11292
11293return <div>{/* Your content */}</div>
11294\`\`\`
11295
11296### 3. Use Consistent Placeholders
11297
11298Instead of spinners, use skeleton screens for consistent layouts:
11299
11300\`\`\`jsx
11301if (loading) {
11302  return <SkeletonCard />
11303}
11304\`\`\`
11305
11306---
11307
11308## Advanced Configuration
11309
11310### Monorepo Setup
11311
11312If your CRA app is in a subdirectory:
11313
11314\`\`\`yaml
11315- name: Install dependencies
11316  working-directory: ./apps/frontend
11317  run: npm ci
11318
11319- name: Build app
11320  working-directory: ./apps/frontend
11321  run: npm run build
11322  env:
11323    CI: false
11324
11325- name: Upload and test
11326  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
11327  with:
11328    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11329    app-directory: "./apps/frontend/build"
11330    rewrites: |
11331      [
11332        { "source": "/(.*)", "destination": "/index.html" }
11333      ]
11334\`\`\`
11335
11336### Custom Public Path
11337
11338If you serve your app from a subdirectory:
11339
11340**In \`package.json\`**:
11341\`\`\`json
11342{
11343  "homepage": "/my-app"
11344}
11345\`\`\`
11346
11347**In workflow**:
11348\`\`\`yaml
11349rewrites: |
11350  [
11351    { "source": "/my-app/(.*)", "destination": "/index.html" }
11352  ]
11353\`\`\`
11354
11355---
11356
11357## Migration to Modern Tools
11358
11359CRA is no longer actively maintained. Consider migrating to:
11360
11361- **Vite**: Faster builds, modern tooling
11362- **Next.js**: Server-side rendering, better performance
11363- **Remix**: Full-stack framework
11364
11365Migration resources:
11366- [Vite Migration Guide](https://vitejs.dev/guide/migration.html)
11367- [Next.js Migration Guide](https://nextjs.org/docs/migrating/from-create-react-app)
11368
11369---
11370
11371## See Also
11372
11373- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
11374- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
11375- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
11376- [React with Vite](${o.REACT_VITE_URL}) - Modern alternative to CRA
11377`,tl=`---
11378{
11379  "title": "Vue 3 with Vite - Complete Setup Guide"
11380}
11381---
11382
11383# {% $frontmatter.title %}
11384
11385Complete guide for setting up Meticulous with Vue 3 applications built with Vite.
11386
11387---
11388
11389## Overview
11390
11391Vue 3 with Vite provides a fast development experience. This guide covers:
11392
11393- **Recorder installation** in \`index.html\`
11394- **CI/CD configuration** with static asset upload
11395- **Authentication handling**
11396- **Common patterns** and troubleshooting
11397
11398**Prerequisites**:
11399- Vue 3 application using Vite
11400- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
11401
11402---
11403
11404## Quick Start
11405
11406### Step 1: Install Recorder in index.html
11407
11408Add the Meticulous recorder script to your \`index.html\` **before any other scripts**.
11409
11410**File**: \`index.html\`
11411
11412\`\`\`html
11413<!DOCTYPE html>
11414<html lang="en">
11415  <head>
11416    <meta charset="UTF-8" />
11417    <link rel="icon" href="/favicon.ico" />
11418    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11419    <title>Your App Name</title>
11420
11421    <!-- Meticulous recorder - MUST be first script -->
11422    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
11423    <script
11424      data-project-id="YOUR_PROJECT_ID"
11425      src="https://snippet.meticulous.ai/v1/meticulous.js"
11426    ></script>
11427  </head>
11428  <body>
11429    <div id="app"></div>
11430    <script type="module" src="/src/main.ts"></script>
11431  </body>
11432</html>
11433\`\`\`
11434
11435**Important**: The recorder must load before your application code.
11436
11437---
11438
11439### Step 2: Configure GitHub Actions Workflow
11440
11441Vue/Vite builds to static files, so we use the \`upload-assets\` action.
11442
11443**File**: \`.github/workflows/meticulous.yml\`
11444
11445\`\`\`yaml
11446${g}
11447
11448      - uses: actions/setup-node@v4
11449        with:
11450          node-version: 20
11451          cache: 'npm'
11452
11453      - name: Install dependencies
11454        run: npm ci
11455
11456      - name: Build app
11457        run: npm run build
11458        env:
11459          NODE_ENV: production
11460
11461      - name: Upload and test
11462        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
11463        with:
11464          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11465          app-directory: "dist"
11466          rewrites: |
11467            [
11468              { "source": "/(.*)", "destination": "/index.html" }
11469            ]
11470\`\`\`
11471
11472---
11473
11474### Step 3: Add API Token Secret
11475
114761. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
114772. Go to GitHub repo → Settings → Secrets and variables → Actions
114783. Create secret named \`METICULOUS_API_TOKEN\` with your token
11479
11480---
11481
11482## How It Works
11483
11484### upload-assets Action
11485
11486The \`upload-assets\` action uploads your built files and runs tests against them.
11487
11488**Build output**: Vite builds to \`dist/\` directory by default.
11489
11490### Rewrites Configuration
11491
11492Handles client-side routing (Vue Router):
11493
11494\`\`\`json
11495[
11496  { "source": "/(.*)", "destination": "/index.html" }
11497]
11498\`\`\`
11499
11500---
11501
11502## Common Patterns
11503
11504### Pattern 1: Detect Test Mode
11505
11506Use \`window.Meticulous.isRunningAsTest\` in your components:
11507
11508**Options API**:
11509\`\`\`vue
11510<template>
11511  <div>
11512    <p v-if="isTest">Running as test</p>
11513    <p v-else>Running normally</p>
11514  </div>
11515</template>
11516
11517<script>
11518export default {
11519  data() {
11520    return {
11521      isTest: window.Meticulous?.isRunningAsTest || false
11522    }
11523  }
11524}
11525</script>
11526\`\`\`
11527
11528**Composition API**:
11529\`\`\`vue
11530<template>
11531  <div>
11532    <p v-if="isTest">Running as test</p>
11533  </div>
11534</template>
11535
11536<script setup>
11537import { ref } from 'vue'
11538
11539const isTest = ref(window.Meticulous?.isRunningAsTest || false)
11540</script>
11541\`\`\`
11542
11543### Pattern 2: Bypass Authentication
11544
11545**File**: \`src/main.ts\`
11546
11547\`\`\`typescript
11548import { createApp } from 'vue'
11549import { createPinia } from 'pinia'
11550import App from './App.vue'
11551import router from './router'
11552
11553const app = createApp(App)
11554
11555// Mock authentication for tests
11556if (window.Meticulous?.isRunningAsTest) {
11557  localStorage.setItem('auth-token', 'test-token')
11558  localStorage.setItem('user', JSON.stringify({
11559    id: 'test-user',
11560    name: 'Test User',
11561    email: '[email protected]'
11562  }))
11563}
11564
11565app.use(createPinia())
11566app.use(router)
11567app.mount('#app')
11568\`\`\`
11569
11570### Pattern 3: Router Navigation Guards
11571
11572Handle authentication in router:
11573
11574**File**: \`src/router/index.ts\`
11575
11576\`\`\`typescript
11577import { createRouter, createWebHistory } from 'vue-router'
11578import type { RouteLocationNormalized } from 'vue-router'
11579
11580const router = createRouter({
11581  history: createWebHistory(import.meta.env.BASE_URL),
11582  routes: [
11583    {
11584      path: '/',
11585      name: 'home',
11586      component: () => import('../views/HomeView.vue')
11587    },
11588    {
11589      path: '/dashboard',
11590      name: 'dashboard',
11591      component: () => import('../views/DashboardView.vue'),
11592      meta: { requiresAuth: true }
11593    }
11594  ]
11595})
11596
11597router.beforeEach((to: RouteLocationNormalized) => {
11598  // Skip auth check during tests
11599  if (window.Meticulous?.isRunningAsTest) {
11600    return true
11601  }
11602
11603  // Normal auth check
11604  if (to.meta.requiresAuth && !isAuthenticated()) {
11605    return { name: 'login', query: { redirect: to.fullPath } }
11606  }
11607
11608  return true
11609})
11610
11611export default router
11612\`\`\`
11613
11614### Pattern 4: Composable for Test Detection
11615
11616Create a reusable composable:
11617
11618**File**: \`src/composables/useMeticulous.ts\`
11619
11620\`\`\`typescript
11621import { ref, readonly } from 'vue'
11622
11623export function useMeticulous() {
11624  const isRunningAsTest = ref(
11625    window.Meticulous?.isRunningAsTest || false
11626  )
11627
11628  const recordCustomValues = (values: Record<string, any>) => {
11629    window.Meticulous?.recordCustomValues?.(values)
11630  }
11631
11632  const getCustomValues = () => {
11633    return window.Meticulous?.getCustomValues?.()
11634  }
11635
11636  return {
11637    isRunningAsTest: readonly(isRunningAsTest),
11638    recordCustomValues,
11639    getCustomValues
11640  }
11641}
11642\`\`\`
11643
11644**Usage**:
11645\`\`\`vue
11646<script setup>
11647import { useMeticulous } from '@/composables/useMeticulous'
11648
11649const { isRunningAsTest } = useMeticulous()
11650</script>
11651\`\`\`
11652
11653---
11654
11655## Complete Example
11656
11657### File Structure
11658
11659\`\`\`
11660your-app/
11661├── src/
11662│   ├── main.ts               # Entry point
11663│   ├── App.vue               # Root component
11664│   ├── router/
11665│   │   └── index.ts          # Router configuration
11666│   ├── stores/               # Pinia stores
11667│   ├── views/                # Page components
11668│   ├── components/           # Reusable components
11669│   └── composables/
11670│       └── useMeticulous.ts  # Test detection composable
11671├── index.html                # Recorder installation
11672├── vite.config.ts            # Vite configuration
11673├── .github/
11674│   └── workflows/
11675│       └── meticulous.yml    # CI/CD
11676└── package.json
11677\`\`\`
11678
11679### Example: Protected View
11680
11681**File**: \`src/views/DashboardView.vue\`
11682
11683\`\`\`vue
11684<template>
11685  <div class="dashboard">
11686    <h1>Welcome, {{ user?.name }}!</h1>
11687    <p>Email: {{ user?.email }}</p>
11688
11689    <div v-if="loading">
11690      <p>Loading dashboard...</p>
11691    </div>
11692    <div v-else-if="error">
11693      <p class="error">{{ error }}</p>
11694    </div>
11695    <div v-else>
11696      <!-- Dashboard content -->
11697    </div>
11698  </div>
11699</template>
11700
11701<script setup lang="ts">
11702import { ref, onMounted } from 'vue'
11703import { useRouter } from 'vue-router'
11704
11705interface User {
11706  id: string
11707  name: string
11708  email: string
11709}
11710
11711const router = useRouter()
11712const user = ref<User | null>(null)
11713const loading = ref(true)
11714const error = ref('')
11715
11716onMounted(async () => {
11717  // Mock data for tests
11718  if (window.Meticulous?.isRunningAsTest) {
11719    user.value = {
11720      id: 'test-user-123',
11721      name: 'Test User',
11722      email: '[email protected]'
11723    }
11724    loading.value = false
11725    return
11726  }
11727
11728  // Normal data fetching
11729  try {
11730    const response = await fetch('/api/user')
11731    if (!response.ok) throw new Error('Failed to fetch user')
11732    user.value = await response.json()
11733  } catch (err) {
11734    error.value = err instanceof Error ? err.message : 'Unknown error'
11735    router.push('/login')
11736  } finally {
11737    loading.value = false
11738  }
11739})
11740</script>
11741\`\`\`
11742
11743---
11744
11745## CI/CD Configuration Details
11746
11747### Environment Variables
11748
11749Vite exposes environment variables prefixed with \`VITE_\`:
11750
11751\`\`\`yaml
11752- name: Build app
11753  run: npm run build
11754  env:
11755    VITE_API_URL: "https://api.example.com"
11756    VITE_APP_NAME: "My App"
11757    NODE_ENV: production
11758\`\`\`
11759
11760**In code**:
11761\`\`\`typescript
11762const apiUrl = import.meta.env.VITE_API_URL
11763\`\`\`
11764
11765### TypeScript Configuration
11766
11767Ensure TypeScript recognizes Vite env variables:
11768
11769**File**: \`src/env.d.ts\`
11770
11771\`\`\`typescript
11772/// <reference types="vite/client" />
11773
11774interface ImportMetaEnv {
11775  readonly VITE_API_URL: string
11776  readonly VITE_APP_NAME: string
11777}
11778
11779interface ImportMeta {
11780  readonly env: ImportMetaEnv
11781}
11782\`\`\`
11783
11784---
11785
11786## Troubleshooting
11787
11788### Issue: Recorder Not Loading
11789
11790**Symptom**: \`window.Meticulous\` is undefined
11791
11792**Checks**:
117931. Verify recorder script in \`index.html\` \`<head>\`
117942. Check project ID is correct
117953. Check for CSP blocking
11796
11797**Fix**: Ensure correct placement:
11798
11799\`\`\`html
11800<head>
11801  <!-- Recorder FIRST -->
11802  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
11803
11804  <!-- Then other elements -->
11805  <title>Your App</title>
11806</head>
11807\`\`\`
11808
11809---
11810
11811### Issue: Routes Return 404
11812
11813**Symptom**: Direct navigation to routes returns 404
11814
11815**Cause**: Missing rewrite configuration
11816
11817**Fix**:
11818
11819\`\`\`yaml
11820rewrites: |
11821  [
11822    { "source": "/(.*)", "destination": "/index.html" }
11823  ]
11824\`\`\`
11825
11826---
11827
11828### Issue: TypeScript Errors
11829
11830**Symptom**: \`Property 'Meticulous' does not exist on type 'Window'.\`
11831
11832**Fix**: Add type declarations
11833
11834**File**: \`src/types/meticulous.d.ts\`
11835
11836\`\`\`typescript
11837interface Meticulous {
11838  isRunningAsTest?: boolean
11839  recordCustomValues?: (values: Record<string, any>) => void
11840  getCustomValues?: () => Record<string, any> | undefined
11841  pause?: () => void
11842  resume?: () => void
11843}
11844
11845declare global {
11846  interface Window {
11847    Meticulous?: Meticulous
11848  }
11849}
11850
11851export {}
11852\`\`\`
11853
11854---
11855
11856### Issue: False Positive Diffs
11857
11858**Common causes**:
118591. Animations not completing
118602. Random data
118613. Timestamps
11862
11863**Fixes**:
11864
11865**Disable animations in tests**:
11866\`\`\`vue
11867<template>
11868  <Transition :duration="transitionDuration">
11869    <div>Content</div>
11870  </Transition>
11871</template>
11872
11873<script setup>
11874import { computed } from 'vue'
11875
11876const transitionDuration = computed(() =>
11877  window.Meticulous?.isRunningAsTest ? 0 : 300
11878)
11879</script>
11880\`\`\`
11881
11882**Deterministic IDs**:
11883\`\`\`typescript
11884const generateId = () => {
11885  if (window.Meticulous?.isRunningAsTest) {
11886    return 'test-id-12345'
11887  }
11888  return crypto.randomUUID()
11889}
11890\`\`\`
11891
11892**Ignore timestamps**:
11893\`\`\`vue
11894<template>
11895  <span class="meticulous-ignore">
11896    {{ new Date().toLocaleString() }}
11897  </span>
11898</template>
11899\`\`\`
11900
11901Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
11902
11903---
11904
11905## Testing Best Practices
11906
11907### 1. Handle Loading States
11908
11909Always show loading states:
11910
11911\`\`\`vue
11912<template>
11913  <div v-if="loading">
11914    <SkeletonLoader />
11915  </div>
11916  <div v-else-if="error">
11917    <ErrorMessage :error="error" />
11918  </div>
11919  <div v-else>
11920    <!-- Content -->
11921  </div>
11922</template>
11923\`\`\`
11924
11925### 2. Use Suspense for Async Components
11926
11927\`\`\`vue
11928<template>
11929  <Suspense>
11930    <template #default>
11931      <AsyncComponent />
11932    </template>
11933    <template #fallback>
11934      <LoadingSpinner />
11935    </template>
11936  </Suspense>
11937</template>
11938\`\`\`
11939
11940### 3. Test Locally First
11941
11942\`\`\`bash
11943# Build your app
11944npm run build
11945
11946# Serve built files
11947npx serve dist
11948
11949# Verify at http://localhost:3000
11950\`\`\`
11951
11952---
11953
11954## Advanced Configuration
11955
11956### Monorepo Setup
11957
11958\`\`\`yaml
11959- name: Install dependencies
11960  working-directory: ./apps/frontend
11961  run: npm ci
11962
11963- name: Build app
11964  working-directory: ./apps/frontend
11965  run: npm run build
11966
11967- name: Upload and test
11968  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
11969  with:
11970    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11971    app-directory: "./apps/frontend/dist"
11972    rewrites: |
11973      [
11974        { "source": "/(.*)", "destination": "/index.html" }
11975      ]
11976\`\`\`
11977
11978### Custom Vite Config
11979
11980**File**: \`vite.config.ts\`
11981
11982\`\`\`typescript
11983import { fileURLToPath, URL } from 'node:url'
11984import { defineConfig } from 'vite'
11985import vue from '@vitejs/plugin-vue'
11986import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'
11987
11988export default defineConfig({
11989  plugins: [vue(), CssSourcemapPlugin()],
11990  resolve: {
11991    alias: {
11992      '@': fileURLToPath(new URL('./src', import.meta.url))
11993    }
11994  },
11995  build: {
11996    outDir: 'dist',
11997    sourcemap: true
11998  }
11999})
12000\`\`\`
12001
12002**Important**: \`build.sourcemap\` covers JavaScript only — it does nothing for CSS. Vite emits no CSS source maps for
12003production builds at all, so without extra help Meticulous cannot attribute stylesheet coverage back to the files in your repo.
12004\`@alwaysmeticulous/recorder-plugin/css-sourcemap\` emits a \`.css.map\` for each CSS asset to close that gap.
12005
12006The plugin disables Vite's CSS minification, which is what makes the maps accurate, so enable it on the build whose coverage
12007Meticulous collects rather than on every production build. See
12008[Viewing source coverage information](${o.ENABLE_SOURCE_COVERAGE_URL}) for the accuracy details and the full set of options.
12009
12010---
12011
12012## See Also
12013
12014- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
12015- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
12016- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
12017`,tc=`---
12018{
12019  "title": "Angular - Complete Setup Guide"
12020}
12021---
12022
12023# {% $frontmatter.title %}
12024
12025Complete guide for setting up Meticulous with Angular applications.
12026
12027---
12028
12029## Overview
12030
12031Angular is a comprehensive framework for building web applications. This guide covers:
12032
12033- **Recorder installation** in \`src/index.html\`
12034- **CI/CD configuration** with static asset upload
12035- **Authentication handling**
12036- **Common patterns** and troubleshooting
12037
12038**Prerequisites**:
12039- Angular application (version 12+)
12040- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
12041
12042---
12043
12044## Quick Start
12045
12046### Step 1: Install Recorder in src/index.html
12047
12048Add the Meticulous recorder script to your \`src/index.html\` **before any other scripts**.
12049
12050**File**: \`src/index.html\`
12051
12052\`\`\`html
12053<!doctype html>
12054<html lang="en">
12055<head>
12056  <meta charset="utf-8">
12057  <title>Your App Name</title>
12058  <base href="/">
12059  <meta name="viewport" content="width=device-width, initial-scale=1">
12060  <link rel="icon" type="image/x-icon" href="favicon.ico">
12061
12062  <!-- Meticulous recorder - MUST be first script -->
12063  <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
12064  <script
12065    data-project-id="YOUR_PROJECT_ID"
12066    src="https://snippet.meticulous.ai/v1/meticulous.js"
12067  ></script>
12068</head>
12069<body>
12070  <app-root></app-root>
12071</body>
12072</html>
12073\`\`\`
12074
12075**Important**: The recorder must load before Angular bootstraps.
12076
12077---
12078
12079### Step 2: Configure GitHub Actions Workflow
12080
12081Angular builds to static files, so we use the \`upload-assets\` action.
12082
12083**File**: \`.github/workflows/meticulous.yml\`
12084
12085\`\`\`yaml
12086${g}
12087
12088      - uses: actions/setup-node@v4
12089        with:
12090          node-version: 20
12091          cache: 'npm'
12092
12093      - name: Install dependencies
12094        run: npm ci
12095
12096      - name: Build app
12097        run: npm run build
12098        env:
12099          NODE_ENV: production
12100
12101      - name: Upload and test
12102        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
12103        with:
12104          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
12105          app-directory: "dist/your-app-name"
12106          rewrites: |
12107            [
12108              { "source": "/(.*)", "destination": "/index.html" }
12109            ]
12110\`\`\`
12111
12112**Important**: Replace \`your-app-name\` with your actual app name from \`angular.json\`.
12113
12114---
12115
12116### Step 3: Add API Token Secret
12117
121181. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
121192. Go to GitHub repo → Settings → Secrets and variables → Actions
121203. Create secret named \`METICULOUS_API_TOKEN\` with your token
12121
12122---
12123
12124## Finding Your App Name
12125
12126The build output directory depends on your app name in \`angular.json\`:
12127
12128**File**: \`angular.json\`
12129
12130\`\`\`json
12131{
12132  "projects": {
12133    "my-angular-app": {
12134      "architect": {
12135        "build": {
12136          "options": {
12137            "outputPath": "dist/my-angular-app"
12138          }
12139        }
12140      }
12141    }
12142  }
12143}
12144\`\`\`
12145
12146Use \`dist/my-angular-app\` as the \`app-directory\` in your workflow.
12147
12148---
12149
12150## Common Patterns
12151
12152### Pattern 1: Detect Test Mode
12153
12154For a quick check, you can access \`window.Meticulous\` directly. For a cleaner, reusable approach, see [Pattern 4: Service for Test Detection](#pattern-4-service-for-test-detection) below.
12155
12156**Component**:
12157\`\`\`typescript
12158import { Component, OnInit } from '@angular/core'
12159
12160@Component({
12161  selector: 'app-dashboard',
12162  templateUrl: './dashboard.component.html'
12163})
12164export class DashboardComponent implements OnInit {
12165  isTest = false
12166
12167  ngOnInit() {
12168    this.isTest = (window as any).Meticulous?.isRunningAsTest || false
12169
12170    if (this.isTest) {
12171      // Skip animations, use test data, etc.
12172    }
12173  }
12174}
12175\`\`\`
12176
12177**Template**:
12178\`\`\`html
12179<div *ngIf="isTest" class="test-indicator">
12180  Running as test
12181</div>
12182\`\`\`
12183
12184### Pattern 2: Bypass Authentication
12185
12186**File**: \`src/app/app.component.ts\`
12187
12188\`\`\`typescript
12189import { Component, OnInit } from '@angular/core'
12190import { Router } from '@angular/router'
12191
12192@Component({
12193  selector: 'app-root',
12194  templateUrl: './app.component.html'
12195})
12196export class AppComponent implements OnInit {
12197  constructor(private router: Router) {}
12198
12199  ngOnInit() {
12200    // Mock authentication for tests
12201    if ((window as any).Meticulous?.isRunningAsTest) {
12202      localStorage.setItem('auth-token', 'test-token')
12203      localStorage.setItem('user', JSON.stringify({
12204        id: 'test-user',
12205        name: 'Test User',
12206        email: '[email protected]'
12207      }))
12208    }
12209  }
12210}
12211\`\`\`
12212
12213### Pattern 3: Route Guards
12214
12215Handle authentication in route guards:
12216
12217**File**: \`src/app/guards/auth.guard.ts\`
12218
12219\`\`\`typescript
12220import { Injectable } from '@angular/core'
12221import { Router, CanActivate } from '@angular/router'
12222import { AuthService } from '../services/auth.service'
12223
12224@Injectable({
12225  providedIn: 'root'
12226})
12227export class AuthGuard implements CanActivate {
12228  constructor(
12229    private authService: AuthService,
12230    private router: Router
12231  ) {}
12232
12233  canActivate(): boolean {
12234    // Skip auth check during tests
12235    if ((window as any).Meticulous?.isRunningAsTest) {
12236      return true
12237    }
12238
12239    // Normal auth check
12240    if (this.authService.isAuthenticated()) {
12241      return true
12242    }
12243
12244    this.router.navigate(['/login'])
12245    return false
12246  }
12247}
12248\`\`\`
12249
12250### Pattern 4: Service for Test Detection
12251
12252Create a reusable service:
12253
12254**File**: \`src/app/services/meticulous.service.ts\`
12255
12256\`\`\`typescript
12257import { Injectable } from '@angular/core'
12258
12259interface MeticulousWindow extends Window {
12260  Meticulous?: {
12261    isRunningAsTest?: boolean
12262    recordCustomValues?: (values: Record<string, any>) => void
12263    getCustomValues?: () => Record<string, any> | undefined
12264  }
12265}
12266
12267@Injectable({
12268  providedIn: 'root'
12269})
12270export class MeticulousService {
12271  get isRunningAsTest(): boolean {
12272    return (window as MeticulousWindow).Meticulous?.isRunningAsTest || false
12273  }
12274
12275  recordCustomValues(values: Record<string, any>): void {
12276    (window as MeticulousWindow).Meticulous?.recordCustomValues?.(values)
12277  }
12278
12279  getCustomValues(): Record<string, any> | undefined {
12280    return (window as MeticulousWindow).Meticulous?.getCustomValues?.()
12281  }
12282}
12283\`\`\`
12284
12285**Usage**:
12286\`\`\`typescript
12287import { Component, OnInit } from '@angular/core'
12288import { MeticulousService } from './services/meticulous.service'
12289
12290@Component({
12291  selector: 'app-my-component',
12292  templateUrl: './my-component.component.html'
12293})
12294export class MyComponent implements OnInit {
12295  constructor(private meticulous: MeticulousService) {}
12296
12297  ngOnInit() {
12298    if (this.meticulous.isRunningAsTest) {
12299      // Test-specific logic
12300    }
12301  }
12302}
12303\`\`\`
12304
12305---
12306
12307## Complete Example
12308
12309### File Structure
12310
12311\`\`\`
12312your-app/
12313├── src/
12314│   ├── app/
12315│   │   ├── app.component.ts      # Root component
12316│   │   ├── app-routing.module.ts # Router configuration
12317│   │   ├── guards/
12318│   │   │   └── auth.guard.ts     # Auth guard
12319│   │   ├── services/
12320│   │   │   ├── auth.service.ts   # Auth service
12321│   │   │   └── meticulous.service.ts
12322│   │   └── components/
12323│   ├── index.html                # Recorder installation
12324│   └── main.ts                   # Bootstrap
12325├── angular.json                  # Angular configuration
12326├── .github/
12327│   └── workflows/
12328│       └── meticulous.yml        # CI/CD
12329└── package.json
12330\`\`\`
12331
12332### Example: Protected Component
12333
12334**File**: \`src/app/components/dashboard/dashboard.component.ts\`
12335
12336\`\`\`typescript
12337import { Component, OnInit } from '@angular/core'
12338import { Router } from '@angular/router'
12339import { MeticulousService } from '../../services/meticulous.service'
12340
12341interface User {
12342  id: string
12343  name: string
12344  email: string
12345}
12346
12347@Component({
12348  selector: 'app-dashboard',
12349  templateUrl: './dashboard.component.html',
12350  styleUrls: ['./dashboard.component.css']
12351})
12352export class DashboardComponent implements OnInit {
12353  user: User | null = null
12354  loading = true
12355  error = ''
12356
12357  constructor(
12358    private router: Router,
12359    private meticulous: MeticulousService
12360  ) {}
12361
12362  async ngOnInit() {
12363    // Mock data for tests
12364    if (this.meticulous.isRunningAsTest) {
12365      this.user = {
12366        id: 'test-user-123',
12367        name: 'Test User',
12368        email: '[email protected]'
12369      }
12370      this.loading = false
12371      return
12372    }
12373
12374    // Normal data fetching
12375    try {
12376      const response = await fetch('/api/user')
12377      if (!response.ok) throw new Error('Failed to fetch user')
12378      this.user = await response.json()
12379    } catch (err) {
12380      this.error = err instanceof Error ? err.message : 'Unknown error'
12381      this.router.navigate(['/login'])
12382    } finally {
12383      this.loading = false
12384    }
12385  }
12386}
12387\`\`\`
12388
12389**File**: \`src/app/components/dashboard/dashboard.component.html\`
12390
12391\`\`\`html
12392<div class="dashboard">
12393  <h1 *ngIf="user">Welcome, {{ user.name }}!</h1>
12394
12395  <div *ngIf="loading">
12396    <p>Loading dashboard...</p>
12397  </div>
12398
12399  <div *ngIf="error" class="error">
12400    <p>{{ error }}</p>
12401  </div>
12402
12403  <div *ngIf="!loading && !error && user">
12404    <p>Email: {{ user.email }}</p>
12405    <!-- Dashboard content -->
12406  </div>
12407</div>
12408\`\`\`
12409
12410---
12411
12412## CI/CD Configuration Details
12413
12414### Environment Variables
12415
12416Angular doesn't have built-in support for runtime environment variables in the browser. Use build-time configuration:
12417
12418**File**: \`src/environments/environment.prod.ts\`
12419
12420\`\`\`typescript
12421export const environment = {
12422  production: true,
12423  apiUrl: 'https://api.example.com'
12424}
12425\`\`\`
12426
12427**In workflow**:
12428\`\`\`yaml
12429- name: Build app
12430  run: npm run build -- --configuration production
12431\`\`\`
12432
12433### Custom Output Directory
12434
12435If you have a custom output directory in \`angular.json\`:
12436
12437\`\`\`json
12438{
12439  "architect": {
12440    "build": {
12441      "options": {
12442        "outputPath": "build"
12443      }
12444    }
12445  }
12446}
12447\`\`\`
12448
12449Update workflow:
12450\`\`\`yaml
12451- name: Upload and test
12452  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
12453  with:
12454    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
12455    app-directory: "build"
12456\`\`\`
12457
12458---
12459
12460## Troubleshooting
12461
12462### Issue: Recorder Not Loading
12463
12464**Symptom**: \`window.Meticulous\` is undefined
12465
12466**Checks**:
124671. Verify recorder script in \`src/index.html\` \`<head>\`
124682. Check project ID is correct
124693. Check for CSP blocking
12470
12471**Fix**: Ensure correct placement:
12472
12473\`\`\`html
12474<head>
12475  <!-- Recorder FIRST -->
12476  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
12477
12478  <!-- Then other elements -->
12479  <meta name="viewport" content="width=device-width, initial-scale=1">
12480</head>
12481\`\`\`
12482
12483---
12484
12485### Issue: TypeScript Errors
12486
12487**Symptom**: \`Property 'Meticulous' does not exist on type 'Window'.\`
12488
12489**Fix**: Add type declarations
12490
12491**File**: \`src/typings.d.ts\`
12492
12493\`\`\`typescript
12494interface Meticulous {
12495  isRunningAsTest?: boolean
12496  recordCustomValues?: (values: Record<string, any>) => void
12497  getCustomValues?: () => Record<string, any> | undefined
12498  pause?: () => void
12499  resume?: () => void
12500}
12501
12502declare interface Window {
12503  Meticulous?: Meticulous
12504}
12505\`\`\`
12506
12507**Update**: \`tsconfig.app.json\`
12508
12509\`\`\`json
12510{
12511  "files": [
12512    "src/main.ts",
12513    "src/typings.d.ts"
12514  ]
12515}
12516\`\`\`
12517
12518---
12519
12520### Issue: Wrong Output Directory
12521
12522**Symptom**: \`app-directory "dist/your-app-name" not found\`
12523
12524**Cause**: App name doesn't match \`angular.json\`
12525
12526**Fix**: Check \`angular.json\` for correct output path:
12527
12528\`\`\`bash
12529# Find your app name
12530cat angular.json | grep "outputPath"
12531
12532# Output example: "outputPath": "dist/my-app"
12533# Use "dist/my-app" in workflow
12534\`\`\`
12535
12536---
12537
12538### Issue: Routes Return 404
12539
12540**Symptom**: Direct navigation to routes returns 404
12541
12542**Cause**: Missing rewrite configuration
12543
12544**Fix**:
12545
12546\`\`\`yaml
12547rewrites: |
12548  [
12549    { "source": "/(.*)", "destination": "/index.html" }
12550  ]
12551\`\`\`
12552
12553---
12554
12555### Issue: False Positive Diffs
12556
12557**Common causes**:
125581. Animations not completing
125592. Random data
125603. Timestamps
12561
12562**Fixes**:
12563
12564**Disable animations in tests**:
12565\`\`\`typescript
12566import { Component, OnInit } from '@angular/core'
12567import { MeticulousService } from './services/meticulous.service'
12568
12569@Component({
12570  selector: 'app-my-component',
12571  animations: [/* your animations */]
12572})
12573export class MyComponent implements OnInit {
12574  animationState = 'initial'
12575
12576  constructor(private meticulous: MeticulousService) {}
12577
12578  ngOnInit() {
12579    // Skip animations in tests
12580    if (this.meticulous.isRunningAsTest) {
12581      this.animationState = 'final'
12582    }
12583  }
12584}
12585\`\`\`
12586
12587**Deterministic values**:
12588\`\`\`typescript
12589generateId(): string {
12590  if ((window as any).Meticulous?.isRunningAsTest) {
12591    return 'test-id-12345'
12592  }
12593  return crypto.randomUUID()
12594}
12595\`\`\`
12596
12597**Ignore timestamps**:
12598\`\`\`html
12599<span class="meticulous-ignore">
12600  {{ currentDate | date:'medium' }}
12601</span>
12602\`\`\`
12603
12604Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
12605
12606---
12607
12608## Testing Best Practices
12609
12610### 1. Handle Loading States
12611
12612Always show loading states:
12613
12614\`\`\`html
12615<div *ngIf="loading">
12616  <app-skeleton-loader></app-skeleton-loader>
12617</div>
12618<div *ngIf="!loading && !error">
12619  <!-- Content -->
12620</div>
12621<div *ngIf="error">
12622  <app-error-message [error]="error"></app-error-message>
12623</div>
12624\`\`\`
12625
12626### 2. Use Resolvers for Data
12627
12628Angular resolvers ensure data is loaded before navigation:
12629
12630\`\`\`typescript
12631import { Injectable } from '@angular/core'
12632import { Resolve } from '@angular/router'
12633import { Observable } from 'rxjs'
12634
12635@Injectable({
12636  providedIn: 'root'
12637})
12638export class UserResolver implements Resolve<User> {
12639  constructor(
12640    private userService: UserService,
12641    private meticulous: MeticulousService
12642  ) {}
12643
12644  resolve(): Observable<User> | Promise<User> | User {
12645    if (this.meticulous.isRunningAsTest) {
12646      return {
12647        id: 'test-user',
12648        name: 'Test User',
12649        email: '[email protected]'
12650      }
12651    }
12652
12653    return this.userService.getUser()
12654  }
12655}
12656\`\`\`
12657
12658### 3. Test Locally First
12659
12660\`\`\`bash
12661# Build your app
12662npm run build
12663
12664# Serve built files
12665npx http-server dist/your-app-name
12666
12667# Verify at http://localhost:8080
12668\`\`\`
12669
12670---
12671
12672## Advanced Configuration
12673
12674### Monorepo Setup
12675
12676\`\`\`yaml
12677- name: Install dependencies
12678  working-directory: ./apps/frontend
12679  run: npm ci
12680
12681- name: Build app
12682  working-directory: ./apps/frontend
12683  run: npm run build
12684
12685- name: Upload and test
12686  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
12687  with:
12688    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
12689    app-directory: "./apps/frontend/dist/frontend"
12690    rewrites: |
12691      [
12692        { "source": "/(.*)", "destination": "/index.html" }
12693      ]
12694\`\`\`
12695
12696### Base Href Configuration
12697
12698If your app is served from a subdirectory:
12699
12700**In \`angular.json\`**:
12701\`\`\`json
12702{
12703  "build": {
12704    "options": {
12705      "baseHref": "/my-app/"
12706    }
12707  }
12708}
12709\`\`\`
12710
12711**In workflow**:
12712\`\`\`yaml
12713rewrites: |
12714  [
12715    { "source": "/my-app/(.*)", "destination": "/index.html" }
12716  ]
12717\`\`\`
12718
12719---
12720
12721## See Also
12722
12723- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}
12723) - General Meticulous setup
12724- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
12725- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
12726`,tu=`---
12727{
12728  "title": "Trigger the Meticulous tests by manually creating deployments on GitHub"
12729}
12730---
12731
12732# {% $frontmatter.title %}
12733
12734If you're using GitHub, and you have the ability to generate public URLs that serve up the code at a particular commit, then you can trigger
12735the Meticulous tests by manually creating deployments on GitHub. By tagging commits in your repository with a link to the deployment URL Meticulous
12736can then automatically run the tests for that commit, and compare the results of the tests between commits to your feature branches and the corresponding
12737base commit on the main/master branch.
12738
12739If you're using Vercel with the Vercel GitHub integration then you can skip this guide: Vercel will automatically tag GitHub commits with the deployments it creates, and
12740Meticulous will work out of the box.
12741
12742### How to manually create deployments on GitHub
12743
12744To begin with:
12745
12746 1. Make sure you have the [Meticulous GitHub app](${i.METICULOUS_GITHUB_APP_INSTALL_URL}) installed.
12747 2. Create a GitHub personal access token with read & write permissions for 'Deployments and deployment statuses' or the \`repo_deployment\`  scope. You can create a token [here](https://github.com/settings/tokens). You can use this token as the bearer token in your requests to the GitHub API.
12748
12749For every commit pushed to a pull request and every commit pushed to the main/master branch, you'll need to set up your CI to:
12750
12751  1. [Create a deployment](https://docs.github.com/en/rest/deployments/deployments?apiVersion=2022-11-28#create-a-deployment) for the commit. You'll need to provide the commit SHA as the 'ref', and you can pass 'meticulous-tests' as the 'environment' (or some other name that will allow you to distinguish it from other environments).
12752  2. [Create a status for your new deployment](https://docs.github.com/en/rest/deployments/statuses?apiVersion=2022-11-28). Set the state to 'in_progress'.
12753  3. Create a public URL that serves up the version of the web application at this commit. If this commit is on the main/master branch then this URL will need to be long lived, since future pull requests may compare against it. In order to avoid [false positive diffs](${o.FIX_FALSE_POSITIVES_URL}) you'll need to make sure to build your app with the same configuration for all commits.
12754  4. Update the status by calling the [create status endpoint](https://docs.github.com/en/rest/deployments/statuses?apiVersion=2022-11-28) again. This time set the state to 'success', and the environment_url to the URL that serves up the commit.
12755
12756Once this has executed you should see the new environment name show up on your project settings page under 'Environments to test against'. Tick the checkbox for this environment, and untick the others.
12757
12758Meticulous will now monitor for new pull requests and new deployments, run the tests against the new deployment when it sees one, and post a link to the results as a comment to your pull request.
12759
12760${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
12761`;var td=e.i(458636),th=e.i(991234);let tp=[{id:"overview",url:"/docs",document:n},{id:"onboarding-guide",url:"/docs/onboarding-guide",document:c},{id:"recorder-installation",url:"/docs/recorder-installation",document:d},{id:"ci",url:"/docs/ci",document:h},{id:"github-actions-v2",url:"/docs/github-actions-v2",document:v},{id:"make-check-blocking",url:"/docs/make-check-blocking",document:`---
12762{
12763  "title": "Require approving diffs before merging a PR"
12764}
12765---
12766
12767# {% $frontmatter.title %}
12768
12769{% tabs %}
12770{% tab label="GitHub" %}
12771
12772If you've installed the [Meticulous GitHub App](https://github.com/apps/alwaysmeticulous) Meticulous will add a check on your PR that is red
12773if there are diffs that haven't been approved yet and becomes green once you click the green 'Approve all Visual Differences' button.
12774This button can be found by clicking on the link in the Meticulous comment.
12775
12776If you wish you can make this check blocking, so developers will be unable to merge a PR which has visual differences until they have clicked the button to acknowledge the differences.
12777
12778To do so click on the *'Settings'* tab of your repo, and then select the *'Branches'* tab:
12779
12780![Meticulous comment](https://assets.meticulous.ai/docs/github-actions-branch-protection-rules.png)
12781
12782Select your main or master branch, tick the box to *'Require status checks to pass before merging'*, and add *'Meticulous Tests'* to the list of status checks that are required:
12783
12784![Meticulous comment](https://assets.meticulous.ai/docs/github-actions-branch-protection-rules-main-branch.png)
12785
12786{% callout type="warning" %}
12787**Multiple projects for the same repository:** If you have more than one Meticulous project connected to the same GitHub repository (e.g. for testing different app variants or environments), the check name will include the project name — for example, *'Meticulous Tests (my-project)'*. Make sure to add the correct check name(s) to your required status checks. If you later add a second project to a repo that previously only had one, the check name will change and you'll need to update your branch protection rules accordingly.
12788{% /callout %}
12789
12790{% /tab %}
12791{% tab label="GitLab" %}
12792
12793Requiring diffs to be approved before merging is not yet supported on GitLab.
12794
12795{% /tab %}
12796{% /tabs %}
12797`},{id:"cloud-replay",url:"/docs/cloud-replay",document:k},{id:"faq-and-troubleshooting",url:"/docs/faq-and-troubleshooting",document:L},{id:"additional-guides",url:"/docs/additional-guides",document:P},{id:"getting-started-backend-testing",url:"/docs/additional-guides/getting-started-backend-testing",document:O},{id:"recorder-script-backend-testing",url:"/docs/additional-guides/recorder-script-backend-testing",document:K}
12797,{id:"backend-recorder",url:"/docs/additional-guides/backend-recorder",document:Q},{id:"architecture-overview",url:"/docs/concepts/architecture-overview",document:Z},{id:"network-recording-and-patching",url:"/docs/concepts/network-recording-and-patching",document:ee},{id:"glossary",url:"/docs/concepts/glossary",document:et},{id:"testing-pool",url:"/docs/how-to/testing-pool",document:es},{id:"recorder-script",url:"/docs/how-to/recorder-script",document:ei},{id:"manually-recording-tests",url:"/docs/how-to/manually-recording-tests",document:el},{id:"detect-diffs-locally",url:"/docs/how-to/detect-diffs-locally",document:ec},{id:"prepare-for-tests",url:"/docs/how-to/prepare-for-tests",document:eu},{id:"window-meticulous-object",url:"/docs/how-to/window-meticulous-object",document:ed},{id:"typescript-types",url:"/docs/how-to/typescript-types",document:`---
12798{
12799  "title": "TypeScript Types for window.Meticulous"
12800}
12801---
12802
12803# {% $frontmatter.title %}
12804
12805TypeScript definitions for the \`window.Meticulous\` object are available in the [\`@alwaysmeticulous/sdk-bundles-api\`](https://www.npmjs.com/package/@alwaysmeticulous/sdk-bundles-api) package.
12806
12807## Installation
12808
12809Install the package as a dev dependency:
12810
12811\`\`\`bash
12812npm install --save-dev @alwaysmeticulous/sdk-bundles-api@latest
12813\`\`\`
12814
12815## Usage
12816
12817Import the type and extend the Window interface:
12818
12819\`\`\`typescript
12820import type { MeticulousPublicApi } from '@alwaysmeticulous/sdk-bundles-api';
12821
12822declare global {
12823  interface Window {
12824    Meticulous?: MeticulousPublicApi;
12825  }
12826}
12827\`\`\`
12828
12829Now you have full type safety when using the \`window.Meticulous\` object:
12830
12831\`\`\`typescript
12832// Detect if running as a test
12833if (window.Meticulous?.isRunningAsTest) {
12834  console.log('Running as a Meticulous test');
12835}
12836
12837// Record session context with type safety
12838window.Meticulous?.context.recordUserId('user-123');
12839window.Meticulous?.context.recordUserEmail('[email protected]');
12840window.Meticulous?.context.recordFeatureFlag('myFlag', true);
12841window.Meticulous?.context.recordCustomContext('userRole', 'admin');
12842
12843const override = window.Meticulous?.context?.getFlagOverride?.('myFlag');
12844if (override?.overridden) {
12845  // Prefer override.value in your resolver, then record that same value
12846}
12847\`\`\`
12848
12849## Full API Reference
12850
12851For the complete type definition, see [public-window-api.ts](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/sdk-bundles-api/src/window-api/public-window-api.ts) in the meticulous-sdk repository.
12852`},{id:"testing-multiple-apps",url:"/docs/how-to/testing-multiple-apps-or-app-variants",document:eh},{id:"testing-feature-flags",url:"/docs/how-to/testing-feature-flags",document:ep},{id:"record-and-replay-on-different-environments",url:"/docs/how-to/record-and-replay-on-different-environments",document:em},{id:"fix-false-positive-diffs",url:"/docs/how-to/fix-false-positive-diffs",document:ek},{id:"record-custom-values",url:"/docs/how-to/record-custom-values",document:eS},{id:"record-session-context",url:"/docs/how-to/record-session-context",document:eT},{id:"ignore-url-patterns",url:"/docs/how-to/ignore-url-patterns",document:`---
12853{
12854  "title": "Ignoring URL patterns"
12855}
12856---
12857
12858# {% $frontmatter.title %}
12859
12860You can configure the Meticulous recorder to ignore certain URL patterns. This can be useful if you have specific network requests that you do not want Meticulous to capture or interfere with.
12861
12862### Usage
12863
12864To ignore URL patterns, set the \`window.METICULOUS_IGNORE_URL_PATTERNS\` array before loading the recorder script.
12865
12866#### Example
12867
12868\`\`\`html
12869<script>
12870  window.METICULOUS_IGNORE_URL_PATTERNS = [
12871    "^https://.*\\.amazonaws\\.com/.*"
12872  ];
12873</script>
12874
12875<!-- Load the recorder script after setting the ignore patterns -->
12876<script
12877  data-recording-token="<YOUR_RECORDING_TOKEN>"
12878  data-is-production-environment="false"
12879  src="https://snippet.meticulous.ai/v1/meticulous.js">
12880</script>
12881\`\`\`
12882
12883### Syntax
12884
12885The patterns are defined as strings and are treated as standard JavaScript regular expressions. They are matched against the full URL using \`String.match()\`.
12886
12887### Why no allowlist?
12888
12889We do not explicitly support an allowlist because Meticulous generally needs to capture all or most network requests to replay user sessions accurately. Relying on external sources for network requests during replay can introduce non-determinism, which affects the reliability of the tests.
12890`},{id:"troubleshoot-auth",url:"/docs/how-to/troubleshoot-auth",document:e_},{id:"troubleshoot-recorder",url:"/docs/how-to/troubleshoot-recorder",document:eI},{id:"troubleshoot-replay-accuracy",url:"/docs/how-to/troubleshoot-replay-accuracy",document:eR},{id:"troubleshoot-failed-simulations",url:"/docs/how-to/troubleshoot-failed-simulations",document:eC},{id:"ensure-recorder-captures-all-requests",url:"/docs/how-to/ensure-recorder-captures-all-requests",document:ex},{id:"enable-source-coverage",url:"/docs/how-to/enable-source-coverage",document:eA},{id:"blocked-requests",url:"/docs/how-to/blocked-requests",document:eM},{id:"configure-ignore-patterns",url:"/docs/how-to/configure-ignore-patterns",document:eE},{id:"built-in-checks",url:"/docs/built-in-checks",document:eB},{id:"built-in-checks-accessibility",url:"/docs/built-in-checks/accessibility",document:ez},{id:"built-in-checks-network-requests",url:"/docs/built-in-checks/network-requests",document:eK},{id:"built-in-checks-react-component-renders",url:"/docs/built-in-checks/react-component-renders",document:eX}
12890,{id:"custom-checks",url:"/docs/custom-checks",document:ej},{id:"custom-checks-writing-a-custom-check",url:"/docs/custom-checks/writing-a-custom-check",document:eF},{id:"custom-checks-recording-custom-data",url:"/docs/custom-checks/recording-custom-data",document:eH},{id:"custom-checks-built-in-snapshot-types",url:"/docs/custom-checks/built-in-snapshot-types",document:e$},{id:"custom-checks-best-practices",url:"/docs/custom-checks/best-practices",document:eq},{id:"handle-file-uploads",url:"/docs/how-to/handle-file-uploads",document:eU},{id:"companion-assets-advanced",url:"/docs/how-to/companion-assets-advanced",document:eL},{id:"incremental-asset-upload",url:"/docs/how-to/incremental-asset-upload",document:`---
12891{
12892  "title": "Incremental Asset Upload"
12893}
12894---
12895
12896# {% $frontmatter.title %}
12897
12898{% callout type="info" title="This is an advanced workflow" %}
12899Most apps should use the standard [\`upload-assets\`](/docs/github-actions-v2) workflow, which uploads your whole build on every commit. Incremental asset upload is an optimisation for **extremely large builds** where only a small fraction of files change between commits. It lets your CI pipeline upload only the chunks that changed, while still running tests against a complete build.
12900{% /callout %}
12901
12902## Overview
12903
12904With incremental asset upload, you split your build into named **chunks** and upload each chunk to Meticulous independently. A chunk that
12905hasn't changed since a previous build is already stored under its version id, so re-uploading it is a no-op. Once every chunk that was
12906modified in the build has been uploaded, you trigger a test run by pointing Meticulous at a **manifest** that specifies the version to use
12907for each chunk, so that Meticulous can re-assemble a full build.
12908
12909This splits the work into two commands:
12910
129111. [\`ci upload-asset-chunk\`](#1-upload-each-chunk) — upload a single chunk (skipped instantly if already uploaded).
129122. [\`ci run-with-uploaded-asset-chunks\`](#2-trigger-the-test-run) — assemble the referenced chunks and trigger a test run.
12913
12914Meticulous downloads the referenced chunks into each test run, assembles them — using each chunk's directory prefix — into a single directory, serves that directory from an asset server, and runs your tests against that localhost URL.
12915
12916---
12917
12918## How Chunks Are Assembled
12919
12920Each chunk has:
12921
12922- **\`chunkName\`** — a logical name for the chunk (e.g. \`app\`, \`vendor\`, \`home-page-app\`). Chunks are deduped by the \`(chunkName, chunkVersionId)\` pair.
12923- **\`chunkVersionId\`** — a version identifier for the chunk's contents. See [Choosing a version id](#choosing-a-version-id).
12924- **\`chunkAssetsDirectory\`** — the local directory whose contents are packaged into the chunk.
12925- **\`chunkAssetsDirectoryPrefix\`** — an optional path prefix prepended to every file in the chunk. Files in \`chunkAssetsDirectory\` are served under this prefix at replay time. Leave it empty to serve the files at the root.
12926
12927When Meticulous assembles a build, it lays each chunk's files down under its prefix. For example, given:
12928
12929- **chunk1** — prefix \`""\`, contains \`file1.js\` and \`sub-folder/file2.js\`
12930- **chunk2** — prefix \`""\`, contains \`file2.js\`
12931- **chunk3** — prefix \`"sub-folder2"\`, contains \`file3.js\`
12932
12933the assembled directory is:
12934
12935\`\`\`
12936file1.js
12937file2.js
12938sub-folder/file2.js
12939sub-folder2/file3.js
12940\`\`\`
12941
12942{% callout type="warning" title="Colliding paths resolve last-wins" %}
12943If two chunks contain a file at the same final path, the chunk **later in the manifest wins** — its copy of the file is served. Meticulous logs a warning for every collision and the CLI prints them, but the run still proceeds. Make sure your chunking scheme partitions files so that no two chunks produce the same final path.
12944{% /callout %}
12945
12946### Choosing a version id
12947
12948It is a **hard requirement** that two chunks with different bytes never share the same version id — otherwise Meticulous may serve stale files and your test results will be incorrect.
12949
12950It is a **soft requirement** that two chunks with identical bytes share the same version id — if this is violated too often you lose the deduplication benefit (the chunk is re-uploaded unnecessarily), but correctness is unaffected.
12951
12952A version id can be:
12953
12954- A hash of the chunk's contents (e.g. the directory's content hash), or
12955- A hash of the inputs to the build task that produced the chunk (e.g. the build cache key).
12956
12957You'll need to provide Meticulous with a manfiest of the versions of **all** chunks to be used, not just the versions of the chunks modified
12958in the build. For this reason using the hash of the inputs to the build task that produced the chunk (e.g. the build cache key) can work
12959well, since for build tasks that have been skipped it's easier to compute the hash of the inputs than it is to compute the hash of the
12960contents of the chunk (hash of the output).
12961
12962---
12963
12964## 1. Upload Each Chunk
12965
12966For every chunk that makes up your build, call \`ci upload-asset-chunk\`:
12967
12968\`\`\`bash
12969npx @alwaysmeticulous/cli ci upload-asset-chunk \\
12970  --apiToken="$METICULOUS_API_TOKEN" \\
12971  --chunkName="home-page-app" \\
12972  --chunkVersionId="ad8a8da9aaaweaad9" \\
12973  --chunkAssetsDirectory="dist/home-page-app" \\
12974  --chunkAssetsDirectoryPrefix="" \\
12975  --commitSha="$CI_COMMIT_SHA"
12976\`\`\`
12977
12978If a chunk with the same \`chunkName\` and \`chunkVersionId\` is already uploaded, the command exits \`0\` after compressing but before uploading.
12979
12980For the first build on your main branch you run you'll need to upload **every** chunk in your application. If you consistently run builds
12981for every commit in the chain from thereon then you'll only need to upload the chunks that changed in that commit -- *assuming
12982that every ancestor commit successfully uploaded all of its changed chunks, all the way back to a commit where you uploaded every chunk*.
12983We therefore recommend either calling \`upload-asset-chunk\` for every commit, or using your build cache to skip chunk versions which you know
12984for certain have already been uploaded, since there is already a cache entry from that build task.
12985
12986For more information on the command run \`npx @alwaysmeticulous/cli ci upload-asset-chunk\`, or
12987[view the source code](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/cli/src/commands/ci/upload-asset-chunk.command.ts
12987).
12988
12989---
12990
12991## 2. Trigger the Test Run
12992
12993Once every chunk has been uploaded, write a manifest that references **every chunk required to assemble the full build** — not just the ones that changed in this commit — and pass it to \`ci run-with-uploaded-asset-chunks\`.
12994
12995The manifest is a JSON array of \`{ name, versionId }\` references:
12996
12997\`\`\`bash
12998cat > assets-manifest.json <<'EOF'
12999[
13000  { "name": "home-page-app", "versionId": "ad8a8da9aaaweaad9" },
13001  { "name": "settings-page-app",   "versionId": "dd8ffdaa9dfedebb3" }
13002]
13003EOF
13004
13005npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
13006  --apiToken="$METICULOUS_API_TOKEN" \\
13007  --assetReferencesManifest="./assets-manifest.json" \\
13008  --commitSha="$CI_COMMIT_SHA" \\
13009  --waitForBase
13010\`\`\`
13011
13012Because the version ids are required, your build needs to know — or be able to regenerate — the version id of **every** chunk that forms the build, including the unchanged ones. Using a content hash or build cache key as the version id (see [Choosing a version id](#choosing-a-version-id)) makes this deterministic.
13013
13014This command **fails if any referenced chunk has not been fully uploaded**, so make sure every chunk in the manifest has been uploaded (step 1) before triggering the run, and that your pipeline handles failed uploads.
13015
13016To restrict the run to sessions starting on specific routes, pass \`--sessionFilter\` — see
13017[Filter Sessions by Start URL](/docs/how-to/filter-sessions-by-start-url).
13018
13019For more information on the command run \`npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks\`, or
13020[view the source code](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/cli/src/commands/ci/run-with-uploaded-asset-chunks.command.ts).
13021
13022### Manifest format
13023
13024The manifest is a non-empty JSON array with no duplicate chunk names. Each entry must be an object in one of two shapes:
13025
13026- \`{ "name": string, "versionId": string }\` — an explicit chunk version (both values non-empty), or
13027- \`{ "name": string, "versionLookup": "latest-in-history" }\` — Meticulous resolves the version from the base test run's history (see [Version lookup for unchanged chunks](#version-lookup-for-unchanged-chunks)).
13028
13029\`\`\`json
13030[
13031  { "name": "charts-app", "versionId": "ad8a8da9aaaweaad9" },
13032  { "name": "plugin-1",   "versionLookup": "latest-in-history" }
13033]
13034\`\`\`
13035
13036Names should be as stable as possible across builds.
13037
13038### Version lookup for unchanged chunks
13039
13040Instead of computing and tracking version ids for chunks that haven't changed, you can reference them with
13041\`{ "name": "<chunk>", "versionLookup": "latest-in-history" }\`. When the test run is triggered, Meticulous walks the base test run and its
13042ancestors (up to 16 levels) and resolves the lookup to the version that chunk had in the **nearest ancestor test run** whose manifest
13043included it. Chunks with explicit \`versionId\`s are used as-is.
13044
13045This means your CI only needs to compute version ids for (and upload) the chunks that changed in each commit; every unchanged chunk can be
13046a one-line lookup entry.
13047
13048Lookups are resolved **once per upload**, against the base of the first run created for it, and the resolved versions are then fixed for
13049that upload. If several runs share the same uploaded chunk set but have different bases, they all serve the versions resolved for the first
13050run — a single uploaded build serves a single set of chunk versions. Every subsequent run still re-checks that those chunks are still stored
13051(see the retention requirement below). In the normal one-run-per-commit CI flow this is invisible; it only matters if you re-run the same
13052upload against a different base.
13053
13054Requirements:
13055
13056- **A base commit is required**, since lookups are resolved from the base test run's history. For GitHub projects Meticulous infers the
13057  base automatically (the same base it would compare the run against), so you normally don't need to pass anything. Pass \`--baseSha\`
13058  (or \`--repoDirectory\`, which infers it) only to override the base explicitly.
13059- **A prior chunked-asset test run must exist** in the base commit's ancestry (within 16 levels) that referenced each looked-up chunk.
13060  For the first run you must upload every chunk with explicit version ids.
13061- **The resolved chunk version must still be stored** — if it was deleted by your data retention policy, the trigger fails with an error
13062  and you'll need to re-upload that chunk with an explicit version id.
13063- **Keep \`--waitForBase\` enabled** (the default). Lookups can only resolve once the base test run exists, so the trigger polls for it;
13064  running without a base is not possible for manifests with lookup entries.
13065
13066{% callout type="warning" title="Only mark truly unchanged chunks as lookups" %}
13067It is a **hard requirement** that a chunk referenced with \`versionLookup\` is byte-for-byte unchanged relative to the base build.
13068Meticulous cannot verify this: if the chunk actually changed, the stale version from the base lineage is served silently and your test
13069results will be incorrect — the same class of failure as reusing a version id for different bytes.
13070{% /callout %}
13071
13072---
13073
13074## 3. Test it
13075
13076\`run-with-uploaded-asset-chunks\` will print out a URL at which you can download the assembled build. Download it and check that the build
13077has been re-assembled correctly, with the correct versions of the correct assets mounted 
13077in the correct folders. If you have a backend
13078running at the URL hardcoded into the assets then you can also spin up a local server to serve the directory and check your app loads and
13079runs correctly.
13080
13081---
13082
13083## Full CI Example
13084
13085An example for a SvelteKit app;
13086
13087\`\`\`bash
13088#!/usr/bin/env bash
13089set -euo pipefail
13090
13091# Build your app into per-chunk directories (e.g. dist/charts-app, dist/plugin-1).
13092#
13093# Note: some build outputs can vary slightly between builds even if the input
13094# source files are identical.
13095#
13096# For example some builds may embed the commit SHA in one of the built assets,
13097# or some builds may not be be fully deterministic. In the case of builds that
13098# embed information like a commit SHA you may want to make sure this is split out
13099# into a seperate chunk, rather than injected into every chunk. In the case
13100# of builds that are not fully deterministic (bytes can change slightly on a rebuild)
13101# you may want to use build caching on a stable cache key to ensure chunk version
13102# ids stay stable when the outputs are functionally identical.
13103yarn build
13104
13105# Split the SvelteKit static build into two chunks along a natural boundary:
13106#   - "html": root-level pages + static assets, served at /
13107#   - "app":  the hashed _app/ bundle, served under the _app prefix
13108
13109rm -rf chunks
13110mkdir -p chunks/html
13111
13112# Root-served files (everything except the _app bundle).
13113find build -maxdepth 1 -mindepth 1 ! -name _app \\
13114  -exec cp -R {} chunks/html/ \\;
13115# The _app bundle is uploaded as its own chunk under the _app prefix.
13116
13117# Version each chunk by a content checksum (SHA1 over relative path + bytes)
13118# so chunks dedupe across commits when their contents are unchanged.
13119hash_dir() {
13120  ( cd "$1" && find . -type f -print0 | sort -z | xargs -0 sha1sum ) | sha1sum | cut -d' ' -f1
13121}
13122HTML_VERSION=$(hash_dir chunks/html)
13123APP_VERSION=$(hash_dir build/_app)
13124
13125# Upload the html chunk (short circuits if already uploaded)
13126npx -y @alwaysmeticulous/cli@latest ci upload-asset-chunk \\
13127  --chunkName=html \\
13128  --chunkVersionId="$HTML_VERSION" \\
13129  --chunkAssetsDirectory=chunks/html \\
13130  --commitSha="\${{ github.sha }}"
13131
13132# Upload the app chunk (short circuits if already uploaded)
13133npx -y @alwaysmeticulous/cli@latest ci upload-asset-chunk \\
13134  --chunkName=app \\
13135  --chunkVersionId="$APP_VERSION" \\
13136  --chunkAssetsDirectory=build/_app \\
13137  --chunkAssetsDirectoryPrefix=_app \\
13138  --commitSha="\${{ github.sha }}"
13139
13140# Trigger the run
13141cat > manifest.json <<JSON
13142[
13143  { "name": "html", "versionId": "$HTML_VERSION" },
13144  { "name": "app",  "versionId": "$APP_VERSION" }
13145]
13146JSON
13147npx -y @alwaysmeticulous/cli@latest ci run-with-uploaded-asset-chunks \\
13148  --commitSha="\${{ github.sha }}" \\
13149  --assetReferencesManifest=manifest.json \\
13150  --waitForBase=true
13151\`\`\`
13152
13153---
13154
13155## Chunk Retention
13156
13157Meticulous persists uploaded chunks so they can be reused across builds. A chunk is retained as long as it has been **used** by a test run in
13158the last N days, even if it was **uploaded** more than N days ago. N is set to match the data retention period you have configured for
13159your project (default ~90 days).
13160
13161---
13162
13163## Summary
13164
13165- Split your build into named chunks and give each a version id derived from its contents or build inputs.
13166- Upload at least the changed chunk on each commit with \`ci upload-asset-chunk\`.
13167- Reference **every** chunk required for the full build in the manifest — with an explicit \`versionId\` for changed chunks, or
13168  \`versionLookup: "latest-in-history"\` for unchanged ones — then trigger the run with \`ci run-with-uploaded-asset-chunks\`.
13169`},{id:"filter-sessions-by-start-url",url:"/docs/how-to/filter-sessions-by-start-url",document:`---
13170{
13171  "title": "Filter Sessions by Start URL"
13172}
13173---
13174
13175# {% $frontmatter.title %}
13176
13177Meticulous automatically replays the set of sessions that will exhaustively test your change. However, when triggering a
13178run with [\`ci run-with-uploaded-asset-chunks\`](/docs/how-to/incremental-asset-upload), you can pass a session filter
13179to restrict the set of sessions executed beyond that, by filtering to only sessions that start on specific routes — for
13180example to only test the part of your app affected by a change. This can be useful in extremely large applications,
13181where your build system may be able to more tightly isolate the blast radius of a change than Meticulous can via static
13182analysis.
13183
13184## Usage
13185
13186Write a JSON file with a \`session-start-url-matches-any-regex\` key listing one or more regexes:
13187
13188\`\`\`bash
13189cat > session-filter.json <<'EOF'
13190{
13191  "session-start-url-matches-any-regex": [
13192    "/checkout/",
13193    "^https://app\\\\.example\\\\.com/settings"
13194  ]
13195}
13196EOF
13197
13198npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
13199  --apiToken="$METICULOUS_API_TOKEN" \\
13200  --commitSha="$CI_COMMIT_SHA" \\
13201  --assetReferencesManifest="./assets-manifest.json" \\
13202  --sessionFilter="./session-filter.json"
13203\`\`\`
13204
13205A session is replayed if its **start URL** — the URL the session started recording on — matches **at least one** of the
13206regexes. The same filtered set of sessions is used for both the head run and any base run created to compare against, so
13207comparisons stay consistent.
13208
13209## When the filter matches no sessions
13210
13211No test run is triggered, and the CLI exits with code \`4\` (every other failure exits with \`1\`). The distinct code lets
13212a pipeline treat "this change touches no recorded flow" as a skip rather than a build failure:
13213
13214\`\`\`bash
13215set +e
13216npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks ... --sessionFilter="./session-filter.json"
13217exit_code=$?
13218set -e
13219if [ "$exit_code" -eq 4 ]; then
13220  echo "No sessions matched the filter — skipping Meticulous for this change."
13221  exit 0
13222fi
13223exit "$exit_code"
13224\`\`\`
13225
13226### Skipping a change that touches no routes
13227
13228If your build system determines that a change affects no routes, pass a filter with an empty list (
13228or only blank
13229strings) rather than skipping the Meticulous step entirely:
13230
13231\`\`\`json
13232{
13233  "session-start-url-matches-any-regex": []
13234}
13235\`\`\`
13236
13237The command still uploads the build and creates the deployment, but triggers no test run and exits with code \`4\`.
13238Keeping the deployment matters for stacked pull requests: a pull request built on top of this commit needs something to
13239compare against, and Meticulous can create that base test run from the deployment on demand. Skipping the step entirely
13240leaves the stacked pull request with no base.
13241
13242## Regex syntax
13243
13244Regexes use [Google's RE2 syntax](https://github.com/google/re2/wiki/Syntax). They are validated before the run is
13245triggered, so a regex that doesn't compile fails fast in the CLI with a clear error.
13246
13247{% callout type="info" title="Filtering only affects which sessions run" %}
13248The session filter narrows a single test run down from the project's selected sessions; it doesn't change which sessions
13249Meticulous records or selects. Runs triggered without \`--sessionFilter\` still replay the full selected set.
13250{% /callout %}
13251`},{id:"retry-test-run",url:"/docs/how-to/retry-test-run",document:`---
13252{
13253  "title": "Retry a test run"
13254}
13255---
13256
13257# {% $frontmatter.title %}
13258
13259In this guide, we'll show you how to retry a Meticulous test run if there are failures. It should hopefully be very rare that you need to do this.
13260The Meticulous team will have already been alerted and be investigating the root cause.
13261
13262To re-run the workflow, you can always push up another commit:
13263
13264\`\`\`bash
13265git commit --allow-empty -m "Retry Meticulous" && git push
13266\`\`\`
13267
13268However sometimes it's faster to just re-run the workflow directly in your CI system. Those instructions depend on your CI provider:
13269
13270{% tabs noTabSelectedByDefault=true %}
13271{% tab label="GitHub Actions" %}
13272
13273Meticulous will show as two checks. The first shows the Meticulous results, and has the Meticulous logo next to it:
13274
13275![The Meticulous check](https://assets.meticulous.ai/docs/retry-test-run/1-meticulous-check.png)
13276
13277The second is your GitHub Actions workflow that triggers Meticulous:
13278
13279![Workflow to retrigger](https://assets.meticulous.ai/docs/retry-test-run/2-workflow-to-retrigger.png)
13280
13281It's this second workflow that you need to re-trigger. To do so click 'View Details'. It will show the workflow steps, where
13282one of those workflow steps is the step that triggers Meticulous:
13283
13284![Checking correct workflow](https://assets.meticulous.ai/docs/retry-test-run/3-checking-correct-workflow.png)
13285
13286Click the 'Re-run this job' button in the top right:
13287
13288![Re-run](https://assets.meticulous.ai/docs/retry-test-run/4-re-run.png)
13289
13290This will open a modal where you can re-run the workflow:
13291
13292![Re-run modal](https://assets.meticulous.ai/docs/retry-test-run/5-re-run-modal.png)
13293
13294{% /tab %}
13295
13296{% tab label="Other CI Runners" %}
13297
13298Use the native retry functionality in your CI provider to re-run the workflow that runs the Meticulous tests, or run:
13299
13300\`\`\`bash
13301git commit --allow-empty -m "Retry Meticulous" && git push
13302\`\`\`
13303
13304This will push up a new commit and trigger a new workflow run.
13305
13306{% /tab %}
13307{% /tabs %}
13308`},{id:"setup-okta-sso",url:"/docs/how-to/setup-okta-sso",document:`---
13309{
13310  "title": "Setting up Okta SSO for Meticulous"
13311}
13312---
13313
13314# {% $frontmatter.title %}
13315
13316This guide will show you how to set up Okta SSO for Meticulous. You will need to be an admin of your Okta instance to follow these steps. If you need any help, please reach out to us at \`[email protected]\`.
13317
13318## Configuration
13319
13320Throughout these instructions, replace \`<CompanyName>\` with the name of your company.
13321
13322### Create the application
13323
13324In the *Applications* tab create a new application called *Meticulous* with:
13325
13326- Sign-in method: OIDC - OpenID Connect
13327- Application type: Web Application
13328- Sign in redirect URI:
13329
13330{% command_card_block %}
13331\`\`\`text
13332https://app.meticulous.ai/auth/realms/meticulous/broker/SSO_<CompanyName>/endpoint
13333\`\`\`
13334{% /command_card_block %}
13335
13336- Sign out redirect URI:
13337
13338{% command_card_block %}
13339\`\`\`text
13340https://app.meticulous.ai/auth/realms/meticulous/broker/SSO_<CompanyName>/endpoint/logout_response
13341\`\`\`
13342{% /command_card_block %}
13343
13344### Configure the application
13345
13346Once the application is created, configure it with the following settings:
13347
13348- Assign the application to all potential users of Meticulous. We do not charge anything for users in the UI (our billing is based on contributors to your codebase), so feel free to add a very broad group here such as the whole Engineering org.
13349- Grab our logo from \`https://app.meticulous.ai/logo512.png\` and add it to the application.
13350- Login initiated by: Either Okta or App
13351- Application visibility: Display application icon to users
13352- Login flow: Redirect to app to initiate login (OIDC Compliant)
13353- Initiate login URI:
13354
13355{% command_card_block %}
13356\`\`\`text
13357https://app.meticulous.ai/api/auth/signin?next=https%3A%2F%2Fapp%2Emeticulous%2Eai&idp=SSO_<CompanyName>
13358\`\`\`
13359{% /command_card_block %}
13360
13361### Send the details to Meticulous
13362
13363Send the following in a secure manner (e.g. a single-use 1Password link that's protected with 2FA) to \`[email protected]\` or your point of contact within Meticulous:
13364
13365  1. The client ID of the application.
13366  2. The client secret of the application.
13367  3. What the Okta domain that your users log in at is (i.e. what is in their address bar when they are authenticating - should be something like \`https://<org>.okta.com/\`).
13368  4. A list of domain name(s) your users' emails might have.
13369  5. What value you used for \`<CompanyName>\`.
13370
13371### [Optional] Configure the SSO claims
13372
13373If you wish to restrict access for certain users, you can configure a custom token claim in Okta. You can set either of these two claims (or both) to overwrite a user's current permissions:
13374
13375- \`meticulous_role\`: The role of the user in your organization. Valid values are \`owner\`, \`member\`, and \`reader\`. Owners have full access including member/project management. Members can view and modify projects, and approve diffs. Readers can only view projects.
13376- \`meticulous_projects\`: A comma-separated list of project names in your organization to give the user access to. You can set this to \`*\` to give the user access to all projects in the organization. Note this is ignored for owners who always have access to all projects.
13377
13378If you do not set these claims then an existing user's permissions will be preserved and new users will be added as \`member\`s with access to all projects in the organization.
13379
13380## FAQs
13381
13382- **Do users still need to be invited to Meticulous once we've set up SSO?** No. If a user comes in via SSO then they will automatically have a Meticulous account created and be added to your org.
13383
13384- **How does SSO work for existing Meticulous users?** When an existing user first logs in via SSO they will receive an email asking them if they want to link their account. After clicking to confirm, they can now use SSO to log in to their existing Meticulous account.
13385
13386- **Can we enforce login via SSO?** Yes! Drop us a message once everything is set up and we can enforce login via SSO for you. All users who have logged in via a non-SSO method will be signed out, and from then on Meticulous users in your org will only be able to sign in via SSO.
13387`},{id:"use-custom-event-api",url:"/docs/how-to/use-custom-event-api",document:eP},{id:"recorder-developer-tools",url:"/docs/how-to/recorder-developer-tools",document:eD},{id:"enabling-full-auth",url:"/docs/how-to/auth/enabling-full-auth",document:eQ},{id:"bypassing-auth",url:"/docs/how-to/auth/bypassing-auth",document:eZ},{id:"recorder-npm-dependency",url:"/docs/session-recording/recorder-npm-dependency",document:e4},{id:"ingest-existing-tests",url:"/docs/session-recording/ingest-existing-tests",document:e6},{id:"controlling-data-recorded",url:"/docs/session-recording/controlling-data-recorded",document:e7},{id:"controlling-when-recording-starts-and-stops",url:"/docs/session-recording/controlling-when-recording-starts-and-stops",document:ts},{id:"csp-exceptions",url:"/docs/session-recording/csp-exceptions",document:`---
13388{
13389  "title": "Content Security Policy (CSP) exceptions for the recorder snippet"
13390}
13391---
13392
13393# {% $frontmatter.title %}
13394
13395If you have a strict Content Security Policy (CSP) in place, you may 
13395need to add the following exceptions to allow the Meticulous recorder to work correctly:
13396
13397 - \`frame-src\`: https://snippet.meticulous.ai
13398 - \`script-src\`: https://snippet.meticulous.ai
13399 - \`script-src\`: https://browser.sentry-cdn.com
13400 - \`connect-src\`: https://cognito-identity.us-west-2.amazonaws.com
13401 - \`connect-src\`: https://user-events-v3.s3-accelerate.amazonaws.com
13402 - \`connect-src\`: *.sentry.io
13403`},{id:"redaction",url:"/docs/session-recording/redaction",document:to},{id:"nextjs-app-router",url:"/docs/frameworks/nextjs/app-router",document:tn},{id:"nextjs-pages-router",url:"/docs/frameworks/nextjs/pages-router",document:ti},{id:"react-vite",url:"/docs/frameworks/react/vite",document:ta},{id:"react-create-react-app",url:"/docs/frameworks/react/create-react-app",document:tr},{id:"vue-vite",url:"/docs/frameworks/vue/vite",document:tl},{id:"angular-cli",url:"/docs/frameworks/angular/angular-cli",document:tc},{id:"create-deployments-on-github",url:"/docs/alternative-ci-setups/create-deployments-on-github",document:tu},{id:"exporting-generated-tests",url:"/docs/export/exporting-generated-tests",document:`---
13404{
13405  "title": "Exporting generated tests"
13406}
13407---
13408
13409# {% $frontmatter.title %}
13410
13411If you wish to re-use the Meticulous tests in another tool there are three main ways you can export tests from Meticulous:
13412
13413  1. Via the Meticulous CLI by running [npx @alwaysmeticulous/cli download session](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/cli/src/commands/download-session/download-session.command.ts#L34)
13414  2. Via the Meticulous SDK by calling [getOrFetchRecordedSessionData](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/downloading-helpers/src/file-downloads/sessions.ts#L42)
13415  3. Via the Meticulous REST API at \`https://app.meticulous.ai/api/\` by calling \`GET sessions/[id]\` (with your API token as the bearer token)
13416
13417Sessions (tests) are returned as JSON, and have two main components:
13418
13419 1. Network requests and responses. These are stored in [HAR format](https://en.wikipedia.org/wiki/HAR_(file_format)) and so can be used by most
13420 testing frameworks that support network stubbing, and are supported natively in Chrome, Safari, Firefox and Edge.
13421 2. User interactions. These are stored as JSON serialized UI events as per the [W3C UI Events spec](https://www.w3.org/TR/uievents/#events-uievent-types), and so are usable by most major testing
13422  frameworks, or by calling \`dispatchEvent\` in a browser.
13423`},{id:"not-yet-run-checks",url:"/docs/ci/not-yet-run-checks",document:`---
13424{
13425  "title": "Create Meticulous check in 'success' state until tests start running"
13426}
13427---
13428
13429# {% $frontmatter.title %}
13430
13431In the default Meticulous setup you'll have a workflow that builds your app and invokes the
13432[Meticulous GitHub Action](${o.GITHUB_ACTIONS_SETUP_URL}) (\`upload-assets\` or \`upload-container\` depending on how your app is served). Say you name this workflow '*trigger-meticulous-tests.yml*'. You can
13433make Meticulous a blocking check by marking the '*${td.METICULOUS_GITHUB_CHECK_NAME}*' check as
13434a [required check](${o.MAKE_CHECK_BLOCKING_URL}). The build process would therefore look like this:
13435
13436 1. Your '*trigger-meticulous-tests.yml*' GitHub workflow is triggered (e.g. when a PR is opened). The '*${td.METICULOUS_GITHUB_CHECK_NAME}*' check
13437    has not been created yet, since the Meticulous tests have not started yet, and since you've marked '*${td.METICULOUS_GITHUB_CHECK_NAME}*' as
13438    a required check the PR will not be able to be merged yet.
13439 2. Once the build and pre-steps complete, the Meticulous action is invoked. This creates a second check on the PR, normally
13440    named '*${td.METICULOUS_GITHUB_CHECK_NAME}*'. This check will show as pending until the tests complete. If there are unapproved differences it
13441    will show as a failure, and will be updated to success when the differences are approved. Since you've marked the '*${td.METICULOUS_GITHUB_CHECK_NAME}*'
13442    check as a required check, the PR will not be able to be merged until the differences are approved.
13443 3. Finally the '*trigger-meticulous-tests.yml*' workflow will complete, and be marked as success.
13444
13445{% callout type="warning" %}
13446**Multiple projects for the same repository:** If you have more than one Meticulous project connected to the same GitHub repository (e.g. for testing different app variants or environments), the check name will include the project name — for example, *'Meticulous Tests (my-project)'* instead of *'${td.METICULOUS_GITHUB_CHECK_NAME}'*. Make sure your required status checks use the correct name. If you later add a second project to a repo that previously only had one, the check name will change and you'll need to update your branch protection rules accordingly.
13447{% /callout %}
13448
13449However having the '*${td.METICULOUS_GITHUB_CHECK_NAME}*' check as a required check can cause issues in a couple of scenarios:
13450
13451 1. If you use merge queues. In this case you don't want Meticulous to post a failed check if diffs are detected at the merge queue stage,
13452    since that would block the merge queue from merging. Any differences should have already been approved before the PR was added to the
13453    merge queue. It is therefore standard to skip the Meticulous workflow for merge queue triggers. However, if Meticulous is a
13454    [required checks](${o.MAKE_CHECK_BLOCKING_URL}) then the merge queue would be indefinitely blocked because it'd be waiting for a check
13455    that is never created.
13456 2. If you don't trigger Meticulous for every pull request. In this case you don't want to block merging PRs where Meticulous doesn't run
13457    (i.e. no Meticulous check was ever created).
13458
13459There are two ways of solving these issues:
13460
13461 1. Tick the '*${th.INITIALIZE_WITH_SUCCESSFUL_CHECK_CHECKBOX_LABEL}*' option in your Meticulous project settings.
13462    This will cause Meticulous to register GitHub webhooks to monitor for new pull requests, new commit pushes, and for pull requests added
13463    to merge queues. It will then create a successful '*${td.METICULOUS_GITHUB_CHECK_NAME}*' check in each of these cases straight away. This
13464    check will start off as 'success' and turn to 'pending' when and if the Meticulous tests start running. If the Meticulous tests never run
13465    then the check will be successful, and the PR can merge. This does however mean that developers will be able to merge pull requests in
13466    the period between the PR being opened and the Meticulous tests being triggered after the build or deployment completes.
13467 2. Use a GitHub action such as [wait-for-checks](https://github.com/marketplace/actions/wait-for-checks) that only waits for checks that
13468    are actually triggered, rather than waiting for a hard coded list of checks even if some of them are never triggered on some PRs. As long
13469    as a GitHub workflow is running or a check pending while the application is being built prior to the Meticulous tests being triggered then
13470    the PR will not be able to be merged until the tests complete. However if you trigger the Meticulous tests indirectly by creating a GitHub
13471    deployment then there is a risk the PR will be mergeable in the handful of seconds between the workflow that creates the deployment completing
13472    and Meticulous receiving the GitHub webhook for the new deployment and starting the tests.
13473`},{id:"agents-setup",url:"/docs/agents/setup",document:`---
13474{
13475  "title": "Setting up Meticulous for agents"
13476}
13477---
13478
13479# {% $frontmatter.title %}
13480
13481Meticulous exposes the same read, analysis, and test-run-triggering operations to agents in a few different ways — pick whichever fits how your agent connects.
13482
13483- [CLI](#cli) — a global \`meticulous\` binary the agent runs in your terminal.
13484- [MCP](#mcp) — the hosted MCP server, for agents that speak MCP.
13485- [Skills](#skills) — pre-defined workflows on top of any of the above.
13486- [Ready to use integrations](#ready-to-use-integrations) — one-click installs for Claude Code and Cursor.
13487- [Org-wide setup](#org-wide-setup) — connect Meticulous for your whole team at once, via an OAuth-based connector or a centrally-managed project token.
13488
13489---
13490
13491## CLI
13492
13493To set up the Meticulous CLI:
13494
13495\`\`\`bash
13496npm install --global @alwaysmeticulous/cli@latest
13497meticulous auth login
13498\`\`\`
13499
13500See [CLI commands](${o.AGENTS_CLI_COMMANDS_URL}) for more details, including more authentication options and the command reference.
13501
13502---
13503
13504## MCP
13505
13506**Claude Code**
13507
13508Run the following in your terminal:
13509
13510\`\`\`bash
13511claude mcp add --transport http Meticulous https://app.meticulous.ai/api/mcp
13512\`\`\`
13513
13514Then, in Claude Code, type \`/mcp\` and choose "Authenticate" for the Meticulous MCP.
13515
13516**Cursor**
13517
13518Add to \`~/.cursor/mcp.json\` (global) or \`.cursor/mcp.json\` (per project):
13519
13520\`\`\`json
13521{
13522  "mcpServers": {
13523    "Meticulous": { "url": "https://app.meticulous.ai/api/mcp" }
13524  }
13525}
13526\`\`\`
13527
13528**Codex/ChatGPT**
13529
13530Add a server with name "Meticulous" and URL \`https://app.meticulous.ai/api/mcp\`, then click "Authenticate".
13531
13532See [MCP server](${o.AGENTS_MCP_SERVER_URL}) for more details, including the full list of available tools.
13533
13534---
13535
13536## Skills
13537
13538To install, and update, the skills into your project using [npx skills](https://github.com/vercel-labs/skills) (for the specified agents):
13539
13540\`\`\`bash
13541npx skills add alwaysmeticulous/skills --skill "*" --agent claude-code --agent codex --agent cursor -y
13542\`\`\`
13543
13544See [Skills & Use cases](${o.AGENTS_SKILLS_URL}) for what skills are, the full list of them, and what each one is for.
13545
13546---
13547
13548## Ready to use integrations
13549
13550**Claude Code MCP**
13551
13552Install the hosted MCP server directly from the [Claude directory](https://claude.ai/directory/meticulous).
13553
13554**Claude Code plugin**
13555
13556For Claude Code, the skills and the MCP server come packaged together as a [plugin](https://code.claude.com/docs/en/discover-plugins), covering both of those steps in one go. It installs all the skills, namespaced as \`/meticulous:<skill-name>\`, and connects the hosted MCP server for you. Run in Claude Code:
13557
13558\`\`\`shell
13559/plugin marketplace add alwaysmeticulous/skills
13560\`\`\`
13561
13562Then install the Meticulous plugin:
13563
13564\`\`\`shell
13565/plugin install meticulous@meticulous
13566\`\`\`
13567
13568Then type \`/mcp\` and choose "Authenticate" for the Meticulous MCP server.
13569
13570**Cursor plugin**
13571
13572Install the MCP server and skills directly from the [Cursor marketplace](https://cursor.com/marketplace/meticulous).
13573
13574---
13575
13576## Org-wide setup {% #org-wide-setup %}
13577
13578The steps above set up Meticulous for a single user. To roll it out to your whole team, there are two approaches:
13579
13580- **OAuth-based connectors** authenticate each user individually — an org admin adds a shared connector once, then every team member still authenticates via OAuth themselves the first time they use it, to activate their own access.
13581- **A project API token** authenticates everyone centrally — configured once by an admin, with no individual OAuth step. This is also the option for agent platforms that take a fixed credential instead of a CLI or MCP client (e.g. Claude Tag).
13582
13583To set up an org-wide MCP connector, navigate to:
13584
13585- **Claude Code:** Organization Settings > Connectors > Add (Web).
13586- **Cursor:** Settings > Integrations & MCP > Team MCP Servers > Add (Remote HTTPS).
13587
13588To configure it with a project API token, so it authenticates every user centrally, do the following:
13589
13590- **URL:** \`https://app.meticulous.ai/api/mcp\`
13591- **Credential type:** Bearer
13592- **Prefix:** \`Bearer\`
13593- **Value:** a project API token
13594
13595Select the project below, then copy the token (careful: grants access to recorded sessions!):
13596
13597{% code_with_project_selector %}
13598{% standalone_api_token /%}
13599{% /code_with_project_selector %}
13600
13601As a concrete example of the general steps above, for Claude Tag (Claude in Slack):
13602
136031. Go to [claude.ai/admin-settings/claude-tag/access-bundles](https://claude.ai/admin-settings/claude-tag/access-bundles).
136042. Under **General > Credentials**, click **Connect**.
136053. Set Name to \`Meticulous\` and Credential type to \`Bearer\`.
136064. Set Allowed websites to \`app.meticulous.ai\`.
136075. Under **Custom headers**, click **Add header**, and set:
13608   - Name: \`Authorization\`
13609   - Prefix: \`Bearer\`
13610   - Value: the project API token created above.
13611
13612---
13613
13614{% footnote %}
13615*There is also an [agent-addressed version of this page](${o.AGENTS_SETUP_FOR_AGENTS_URL}), written for the agent rather than for you. That's what we point agents at in various places like CI comments, so they can get set up without you having to explain any of the above — feel free to hand your agent the link directly.*
13616{% /footnote %}
13617`},{id:"agents-setup-for-agents",url:"/docs/agents/setup-for-agents",document:`---
13618{
13619  "title": "Meticulous for coding agents"
13620}
13621---
13622
13623# {% $frontmatter.title %}
13624
13625This page is addressed to AI coding agents. If you're a human setting Meticulous up for your agent, you probably want the [setup guide](${o.AGENTS_SETUP_URL}
13625) instead — it covers the same ground, aimed at you rather than at the agent.
13626
13627{% agent_instructions_heading /%}
13628
13629${(0,td.buildAgentInstructionsBody)()}
13630`},{id:"agent-swarm",url:"/docs/agents/agent-swarm",document:`---
13631{
13632  "title": "Set up Agent swarm"
13633}
13634---
13635
13636# {% $frontmatter.title %}
13637
13638Agent swarm uses a Meticulous-hosted agent to explore and test your application for a pull request. Meticulous uploads the build from your CI job and opens it in a recorder-instrumented browser, where the agent follows your instructions to exercise happy paths, edge cases, and potential regressions. The Agent swarm page reports each test case's result with step-by-step screenshots, and the discovered flows are saved as sessions that extend your replay-test coverage.
13639
13640Use this guide to add Agent swarm to your CI workflow. It complements ordinary Meticulous replay tests; it does not replace the sessions you record or the normal PR-test workflow.
13641
13642{% callout type="warning" title="Contact us before adding this to CI" %}
13643Agent swarm is currently in beta and is **not** enabled until we turn it on for your project. **Contact us before merging the workflow** and wait for confirmation that your project is opted in. If you add the workflow first, launches will fail until we enable it.
13644
13645When you reach out, include your Meticulous org/project, whether you plan to use local mocks or a staging backend, and — if you need staging login — the details in [Login setup](#login-setup).
13646{% /callout %}
13647
13648## Before you start
13649
13650You need:
13651
13652- A Meticulous project linked to the repository and opted in to the beta (see above).
13653- A project-scoped API token stored in your CI provider as \`METICULOUS_API_TOKEN\`.
13654- A build command that produces a directory of static frontend assets.
13655- A GitHub Actions workflow (or equivalent CI job) that runs when a pull request is opened, plus a way to re-run manually.
13656
13657Choose one environment for the agent:
13658
13659| Your application | Recommended setup |
13660| --- | --- |
13661| Static site, or frontend whose API responses can be mocked | Upload the build only. |
13662| Static frontend with a disposable staging API | Upload the build and proxy selected paths, such as \`/api\`, to that API. |
13663| App that can run in a Docker image | Upload the container with \`--localImageTag\`; see the [CLI command reference](${o.AGENTS_CLI_COMMANDS_URL}). |
13664
13665The rest of this guide walks through the first two options.
13666
13667---
13668
13669## 1. Add agent instructions
13670
13671Create and commit \`.meticulous/agent-swarm-instructions.md\`. Describe the app in terms the agent can act on: routes, important controls, test accounts, and the expected flows. Keep secrets out of this file.
13672
13673Meticulous reads this file from the repository at the commit being tested for every Agent swarm run, whether it was launched from CI with \`ci agent-test\` or triggered automatically. This means a pull request can change its own instructions, but uncommitted changes are not visible to Meticulous.
13674
13675For example:
13676
13677\`\`\`md
13678# Storefront
13679
13680Start at \`/\`. Browse the catalogue, add an item to the cart, and complete the checkout form. Use \`[email protected]\` and the test account details supplied by the environment. Also visit \`/orders\` and check empty and populated states.
13681\`\`\`
13682
13683Good instructions name the goal and the UI affordances that lead to it. They should also call out variants worth exercising, such as validation errors, filters, feature flags, or a dark-mode toggle.
13684
13685If a first-time tour, tip, or consent banner can cover the application, name it and its visible dismiss control in the instructions. Agent swarm automatically closes common consent banners and vendor tours before starting a flow, and it remembers any other disposable overlay it has had to dismiss so later runs close it automatically too. To have an overlay closed from the very first run, add a \`data-meticulous-overlay-dismiss\` attribute to its dismiss control, or name the overlay and its dismiss control in the instructions. Do not tell the agent to close a dialog that is part of the behavior you want tested.
13686
13687For applications with configured staging login, the most deterministic option is to complete the onboarding state as part of that project login setup. Meticulous captures the resulting cookies and local storage and seeds each case browser with them, so the popup never appears or adds a dismissal action to the recorded session. Tell us which tour-completion state or action is required when you send the [login setup](#login-setup) details.
13688
13689### Overriding the instructions file
13690
13691Pass \`--instructionsFile <path>\` to use a different markdown file for one CLI-launched run. The override replaces \`.meticulous/agent-swarm-instructions.md\`; the two files are not merged. This is useful for environment- or mode-specific instructions.
13692
13693If you already use \`.github/agent-review/instructions.md\`, move it to \`.meticulous/agent-swarm-instructions.md\` so automatically triggered runs can find it. Alternatively, keep passing the old path with \`--instructionsFile\` for CLI-launched runs.
13694
13695---
13696
13697## 2. Build and launch the static frontend
13698
13699Add a separate pull-request workflow, or a job after your existing build. Agent swarm runs are relatively expensive, so prefer **not** running on every push. Trigger on PR **opened** / **reopened**, and use \`workflow_dispatch\` when you want a fresh run after later commits.
13700
13701The following GitHub Actions job builds a static site in \`build/\` and launches Agent swarm for the PR's **head commit**:
13702
13703\`\`\`yaml
13704name: Agent swarm
13705
13706on:
13707  pull_request:
13708    types: [opened, reopened]
13709  workflow_dispatch:
13710
13711jobs:
13712  generate-sessions:
13713    runs-on: ubuntu-latest
13714    env:
13715      METICULOUS_API_TOKEN: \${{ secrets.METICULOUS_API_TOKEN }}
13716    steps:
13717      - uses: actions/checkout@v4
13718      - uses: pnpm/action-setup@v4
13719      - uses: actions/setup-node@v4
13720        with:
13721          node-version: "22"
13722          cache: pnpm
13723      - run: pnpm install --frozen-lockfile
13724      - run: pnpm build
13725      - name: Launch Agent swarm
13726        run: |
13727          npx -y @alwaysmeticulous/cli@latest ci agent-test \\
13728            --assetsDir build \\
13729            --commitSha "\${{ github.event.pull_request.head.sha || github.sha }}"
13730\`\`\`
13731
13732Replace \`build\` with your build output directory. The command runs the agent in Meticulous; your CI runner does not need Docker or LLM credentials.
13733
13734Do **not** use bare \`on: pull_request:\` (that fires on every push to the PR). To re-run after later commits, use **Actions → Agent swarm → Run workflow**.
13735
13736{% callout type="warning" title="Use the pull request head SHA" %}
13737On a \`pull_request\` event, \`github.sha\` can be GitHub's temporary merge commit. Pass \`github.event.pull_request.head.sha || github.sha\` so the generated sessions and their result are associated with the commit that Meticulous tests and displays for the PR.
13738{% /callout %}
13739
13740To prove the command and paths are valid without launching an agent, append \`--dryRun\`. You can also run the same command locally after building, with \`METICULOUS_API_TOKEN\` exported.
13741
13742---
13743
13744## 3. Connect a staging backend (when needed)
13745
13746If the uploaded frontend needs a live backend, serve a disposable staging environment over public HTTPS and proxy the frontend's relative API paths to it:
13747
13748\`\`\`yaml
13749      - name: Launch Agent swarm with staging API
13750        env:
13751          METICULOUS_STAGING_USERNAME: \${{ secrets.STAGING_AGENT_USERNAME }}
13752          METICULOUS_STAGING_PASSWORD: \${{ secrets.STAGING_AGENT_PASSWORD }}
13753        run: |
13754          npx -y @alwaysmeticulous/cli@latest ci agent-test \\
13755            --assetsDir frontend/dist \\
13756            --backendUrl "https://staging.example.com" \\
13757            --backendProxyPaths /api \\
13758            --commitSha "\${{ github.event.pull_request.head.sha || github.sha }}"
13759\`\`\`
13760
13761Your frontend must make same-origin requests under the configured prefixes (for example, \`fetch('/api/tasks')\`). \`--backendProxyPaths\` defaults to \`/api\`, so you can omit it when that is the only prefix you need. Absolute API URLs bypass this proxy; contact Meticulous to configure an egress policy for every required cross-origin host. The backend must be publicly reachable on HTTPS, accept the proxied requests, and use a disposable account because the agent can perform writes.
13762
13763### Login setup
13764
13765If the agent needs to sign in against your staging backend, we configure the login flow for your project — it is not something you set in CI. Send us the following **before** your first credentialed run (ideally when you request beta access):
13766
13767| Tell us | Why we need it |
13768| --- | --- |
13769| How users sign in (username/password on the app, redirect to IdP/SSO, magic link, etc.) | 
13769Chooses / customizes the login flow; popups and many OAuth/SSO flows are not supported today |
13770| Login page URL or path (e.g. \`/login\`, or "opens at app root") | The standard username/password flow starts at the app URL and expects the form there |
13771| Username and password field selectors if non-standard | Defaults are \`input[type="email"]\` / \`input[name="email"]\` / \`input[name="username"]\`, \`input[type="password"]\`, and \`button[type="submit"]\` / \`input[type="submit"]\` |
13772| Submit control (button text / selector) | Same as above |
13773| What "logged in" looks like (URL after login, cookie names, or a screenshot) | Confirms the flow succeeded before the agent starts |
13774| Any first-time tour or tip that appears after login, and how to complete it | Lets us capture and seed the completed onboarding state before each case |
13775| Staging origin (\`https://…\`) | Wired as \`--backendUrl\` |
13776| Whether MFA / captcha / bot checks apply to the test account | Usually blocks automated login unless disabled for that account |
13777
13778You can paste this into Slack or email:
13779
13780\`\`\`text
13781Project: <org/project>
13782Staging backend URL: https://…
13783Login type: username/password on app | SSO/IdP | other (describe)
13784Login URL/path: …
13785Username field: (default ok / selector: …)
13786Password field: (default ok / selector: …)
13787Submit: (default ok / selector or button text: …)
13788After login: lands on … / session cookie …
13789Post-login tour/tip: none | completion action/state: …
13790Test account: (username/password go in CI secrets only)
13791MFA/captcha on staging for this account: yes/no
13792\`\`\`
13793
13794Once we have configured login, provide credentials and any other login options only through \`METICULOUS_STAGING_*\` environment variables: \`METICULOUS_STAGING_USERNAME\` and \`METICULOUS_STAGING_PASSWORD\`, plus any flow-specific values we agree on — for example \`METICULOUS_STAGING_TOTP_SECRET\` (base32 TOTP seed) for an MFA flow, or \`METICULOUS_STAGING_SKIP_EMAIL_CLIENT_ID\` (trusted-automation client id) when the login flow must bypass an email verification challenge. Every environment variable with the \`METICULOUS_STAGING_\` prefix is forwarded to the login flow, so adding a new option never requires a CLI upgrade. Do not put these values in agent instructions or commit them to the repository.
13795
13796---
13797
13798## 4. Verify the first run
13799
138001. Open a pull request with a small, visible change (or manually re-run the workflow).
138012. Confirm the **Agent swarm** job uploads the build and prints a workflow run identifier.
138023. Open the Meticulous test run for the PR commit, then open **Agent swarm** to inspect the generated test cases, results, and screenshots.
138034. Review the recorded flows and adjust \`.meticulous/agent-swarm-instructions.md\` if important routes or states were missed.
13804
13805If the agent cannot reach an API request, first check that the frontend uses a relative URL under \`--backendProxyPaths\`, or ask Meticulous to add an egress policy for its cross-origin host; then check that the staging deployment is healthy and the test credentials can sign in. For command syntax and the container option, see [CLI commands](${o.AGENTS_CLI_COMMANDS_URL}).
13806`},{id:"agents-whats-new",url:"/docs/agents/whats-new",document:`---
13807{
13808  "title": "What's new"
13809}
13810---
13811
13812# {% $frontmatter.title %}
13813
13814Unless a change is specific to one surface, changes listed here apply equally to the CLI and the corresponding MCP tools.
13815
13816### October 2, 2026 — list test runs and review non-visual checks
13817
13818- \`meticulous agent test-runs\` **(new)** lists a project's pull request test runs, newest first, or its base test runs with \`--baseTestRuns\`. Options filter the runs and add more detail to each one.
13819- \`meticulous agent reject-check\`, \`approve-check\` and \`ignore-check\` **(new)** record an agent decision on a failing builtin or custom check, the same way the diff commands do for screenshot diffs. Rejecting is always available; approving and ignoring need **Enable approve/ignore check actions** under the project's Agents settings. An agent can never approve or ignore a check a person rejected. Like the other test-run commands, they take \`--prNumber\` as an alternative to \`--testRunId\`.
13820- \`meticulous agent check-comments\` **(new)** reads back the reasons given with those decisions, which are stored as their justification.
13821
13822\`\`\`bash
13823meticulous agent test-runs --prNumber=123
13824meticulous agent reject-check --testRunId="<id>" --checkId="network-requests" --reason="..."
13825meticulous agent check-comments --testRunId="<id>" --checkId="network-requests"
13826\`\`\`
13827
13828---
13829
13830### September 30, 2026 — look up a test run by pull request number
13831
13832- Every command that resolves a test run from \`--testRunId\` or \`--commitSha\` — \`test-run-for-commit\`, \`test-run-diffs\`, \`test-run-check\`, \`js-coverage\` and \`js-coverage-diff\` — now also accepts \`--prNumber\`, which resolves to the latest test run for the pull request's head commit, exactly as \`--commitSha\` would for that commit. On the MCP server, the test-run tools take \`prNumber\` in place of \`testRunId\`.
13833
13834\`\`\`bash
13835meticulous agent test-run-diffs --prNumber=123
13836\`\`\`
13837
13838---
13839
13840### September 29, 2026 — approve and ignore diffs
13841
13842- \`meticulous agent approve-diff\` **(new)** approves a screenshot diff, optionally with a review comment explaining why (\`--reason\` with \`--x\`/\`--y\`). It is only available on projects that turn on **Enable approve/ignore diff actions** under the project's Agents settings; on those projects \`ignore-diff\` also records a real ignore rather than just a comment, so an agent can pass the Meticulous check without a human review. An agent can never approve or ignore a diff a person rejected.
13843
13844\`\`\`bash
13845meticulous agent approve-diff --replayDiffId="<id>" --screenshotName="<name>"
13846\`\`\`
13847
13848---
13849
13850### September 28, 2026 — promote recorded sessions into the selected set
13851
13852- \`meticulous agent promote-sessions\` **(new)** adds sessions you recorded to the selected set straight away, rather than waiting for the next session selection. Pass the test run you triggered over them with \`trigger-test-run --sessionIds\`. The commit's coverage reflects the promotion as soon as the updated base run it creates has finished post-processing.
13853
13854\`\`\`bash
13855meticulous agent trigger-test-run --sessionIds="<id1>,<id2>"
13856meticulous agent promote-sessions --testRunId="<id>"
13857\`\`\`
13858
13859---
13860
13861### September 28, 2026 — \`agent upload-build\` takes a build directory only
13862
13863\`meticulous agent upload-build\` no longer accepts \`--appZip\`. Pass the build output directory instead; it uploads in the format replays can start from without unpacking. This change is CLI-only: the MCP asset upload still takes a zip.
13864
13865\`\`\`bash
13866meticulous agent upload-build --appDirectory="<path-to-build>"
13867\`\`\`
13868
13869---
13870
13871### September 28, 2026 — filter sessions by what recorded them
13872
13873\`meticulous agent sessions\` can drop sessions recorded by Agent swarm (\`--excludeAgentReviewSessions\`), keep only sessions that captured their page's HTML document (\`--requireInitialNavigationResponse\`), and show what recorded each session (\`--includeSource\`).
13874
13875\`\`\`bash
13876meticulous agent sessions --excludeAgentReviewSessions --includeSource
13877meticulous agent sessions --requireInitialNavigationResponse --includeStartUrl
13878\`\`\`
13879
13880---
13881
13882### September 15, 2026 — retrieve, filter and order the selected set
13883
13884- \`meticulous agent sessions --selectedSet\` narrows the listing to the selected set Met
13884iculous replays.
13885- \`--includeSelectedSince\` adds when each session entered that set.
13886- \`--includeAdditionalCoverage\` adds the coverage each session contributed over everything picked before it.
13887- \`--orderBy\` chooses the order (\`rank\`, \`selectedSince\`, \`additionalCoverage\`), and \`--order\` sets the direction (\`asc\`/\`desc\`).
13888
13889\`\`\`bash
13890meticulous agent sessions --selectedSet --includeSelectedSince
13891meticulous agent sessions --selectedSet --orderBy=rank
13892meticulous agent sessions --selectedSet="2026-07-01"
13893meticulous agent sessions --selectedSet --orderBy=selectedSince --order=asc
13894meticulous agent sessions --selectedSet --includeAdditionalCoverage --orderBy=additionalCoverage
13895\`\`\`
13896
13897---
13898
13899### September 11, 2026 — export reporting statistics
13900
13901Three new commands export test-run, daily-project, and test-run-event reporting statistics in machine-readable pages, scoped by \`--since\` (\`--until\` defaults to now) or by identifiers.
13902
13903\`\`\`bash
13904meticulous agent test-run-stats --since="2026-08-01"
13905meticulous agent project-daily-stats --since="2026-08-01"
13906meticulous agent test-run-event-stats --testRunIds="<id1>,<id2>" --json
13907\`\`\`
13908
13909---
13910
13911### September 11, 2026 — coverage you can triage, total, and compare
13912
13913- \`meticulous agent js-coverage --summary\` **(new)** reports a run's overall coverage — executed, executable and uncovered line counts and the coverage percentage — instead of the per-file list.
13914- \`--includeLineCounts\` **(new)** adds per-file \`executedLines\`/\`executableLines\`/\`uncoveredLines\` instead of ranges.
13915- \`--orderBy\` and \`--limit\`/\`--offset\` **(new)** order and page the output server-side.
13916- \`--globFilter\` is now repeatable, matching any of several globs.
13917- \`meticulous agent js-coverage-diff\` now diffs a whole test run against the base run it was compared against, and shares most of its arguments with \`js-coverage\`.
13918
13919\`\`\`bash
13920meticulous agent js-coverage --summary
13921meticulous agent js-coverage --includeLineCounts --orderBy=uncoveredLines --limit=50
13922meticulous agent js-coverage-diff --summary
13923\`\`\`
13924
13925---
13926
13927### September 4, 2026 — link to a group of diffs
13928
13929- Gallery group headers have "Copy link to group" in Similar, URL, User flow, and custom groupings. The link opens that grouping and scrolls to the group.
13930- For a Similar-group link, \`meticulous agent test-run-diffs --similarGroupId="<id>"\` returns every screenshot in that group. \`--includeSimilarGroupId\` adds the id on each row.
13931
13932\`\`\`bash
13933meticulous agent test-run-diffs --similarGroupId="<id>" --testRunId="<id>"
13934\`\`\`
13935
13936---
13937
13938### August 17, 2026 — get real coverage for a base commit
13939
13940- \`meticulous agent complete-base-run\` **(new)** replays the rest of a base run's selected sessions, to complete its coverage information.
13941- \`meticulous agent js-coverage\` now refuses a base run whose selected set hasn't fully replayed, saying how many sessions are missing, rather than reporting an understated total.
13942
13943\`\`\`bash
13944meticulous agent complete-base-run
13945meticulous agent js-coverage
13946\`\`\`
13947
13948---
13949
13950### August 11, 2026 — discover available check IDs, and a renamed check command
13951
13952- \`meticulous agent test-run-checks\` is renamed to \`meticulous agent test-run-check\`, since it operates on a single check.
13953- The new \`--availableIds\` flag (MCP: \`get_test_run_check_available_ids\`) lists the check IDs that have reported results for a test run.
13954
13955\`\`\`bash
13956meticulous agent test-run-check --availableIds --testRunId="<id>"
13957meticulous agent test-run-check --checkId="accessibility" --testRunId="<id>"
13958\`\`\`
13959
13960---
13961
13962### August 10, 2026 — agent diff reviews and review comment writes
13963
13964- \`meticulous agent reject-diff\` records a rejection with a review comment explaining why.
13965- \`meticulous agent ignore-diff\` says a diff looks like an unrelated variant / flake, currently as a comment only.
13966- The new \`create-diff-comment\` and \`reply-to-diff-comment\` commands let agents start and continue review threads independently of a decision.
13967- \`meticulous agent diff-comments\` gained an \`isAgentAuthored\` attribute, distinguishing agent-written comments from human ones.
13968
13969\`\`\`bash
13970meticulous agent reject-diff --replayDiffId="<id>" --screenshotName="<name>" --reason="..." --x=0.5 --y=0.5
13971meticulous agent reply-to-diff-comment --commentId="<id>" --text="..."
13972\`\`\`
13973
13974---
13975
13976### August 7, 2026 — non-visual check reports, session activity counts, and see and change which project you're querying
13977
13978- \`meticulous agent test-run-checks\` retrieves the Markdown report for a non-visual check. For customer-reported checks, use \`--checkType="custom"\`.
13979- \`meticulous agent sessions\` gained \`--includeNumberUserEvents\` and \`--includeNumberUrlsVisited\` for adding the recorded user-event and URL-visit counts to each session, and \`--includeDurationSeconds\` for adding each session's duration in seconds (omitted for sessions where a duration couldn't be computed, e.g. recorded before this was tracked).
13980- The MCP server gained \`whoami\`, \`list_projects\`, \`get_project\` and \`set_project\`, matching the existing \`meticulous auth\` commands — so an agent can now check which project its calls resolve to, and change it, without leaving the connection. \`meticulous auth get-project\` and \`set-project\` also gained \`--json\`.
13981- Relatedly, an empty test-run or session lookup now names the project it searched and how to change it, rather than just reporting nothing found: a default project lives on your user account, so it is shared across machines and sessions and an unexpectedly empty result is usually the wrong project rather than missing data.
13982
13983\`\`\`bash
13984meticulous agent test-run-checks --checkId="accessibility"
13985meticulous agent test-run-checks --checkId="network-requests" --testRunId="<id>"
13986meticulous agent sessions --includeDurationSeconds
13987meticulous agent sessions --includeNumberUserEvents
13988meticulous agent sessions --includeNumberUrlsVisited
13989meticulous auth get-project --json
13990\`\`\`
13991
13992---
13993
13994### August 4, 2026 — filter and focus test-run diffs
13995
13996- \`meticulous agent test-run-diffs --onlyWithComments\` filters to screenshot diffs with at least one open review comment.
13997- The \`--only*\` row filters now combine as a union, so passing several returns the diffs matching any of them.
13998- \`meticulous agent test-run-diffs\` returns all diffs when there are at most five; above that, a selected representative subset in priority order. Full-diff results no longer include an \`isSelected\` field. \`--onlyRejected\`/\`--onlyWithComments\` are unaffected by this cap, they always return every matching diff.
13999- \`meticulous agent test-run-diffs --counts\` now also reports \`numWithOpenComments\`.
14000
14001---
14002
14003### August 3, 2026 — review comments, rejected diffs, and leaner test-run-diffs output
14004
14005- \`meticulous agent test-run-diffs --includeReviews\` adds \`decision\` (previously \`--includeReviewDecisions\`) and \`openComments\` to each diff.
14006- \`meticulous agent diff-comments --replayDiffId="<id>" --screenshotName="<name>"\` retrieves the corresponding open comments with nested replies.
14007- \`meticulous agent test-run-diffs --onlyRejected\` returns every screenshot diff already marked rejected, across every difference rather than only the selected subset.
14008- \`meticulous agent test-run-diffs\` no longer returns \`index\` or \`outcome\`, and now returns \`mismatchFraction\` only with \`--includeMismatchFraction\`.
14009
14010\`\`\`bash
14011meticulous agent test-run-diffs --includeReviews
14012meticulous agent diff-comments --replayDiffId="<id>" --screenshotName="<name>"
14013meticulous agent test-run-diffs --onlyRejected
14014meticulous agent test-run-diffs --includeMismatchFraction
14015\`\`\`
14016
14017---
14018
14019### July 24, 2026 — submit feedback about Meticulous
14020
14021- \`meticulous agent submit-feedback\` **(new)** lets an agent send free-form feedback to the Meticulous team — whether Meticulous helped catch or debug a problem, what was confusing, and what information would have made the task easier. Optionally tag it with \`--outcome\` (\`helped\`/\`neutral\`/\`hindered\`), the related \`--testRunId\`, the \`--skill\` being followed, and \`--agentName\`/\`--agentModel\`.
14022
14023\`\`\`bash
14024meticulous agent submit-feedback --message="Caught a real regression in the checkout flow" --outcome="helped" --testRunId="<id>" --skill="meticulous-review"
14025\`\`\`
14026
14027---
14028
14029### July 21, 2026 — OAuth device flow login and project-level JS coverage
14030
14031- \`meticulous auth login --device\` **(new)** logs in via the [OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628): the CLI prints a URL and code you can open and confirm in a browser on any device, then polls until the grant is confirmed. Use this on remote or sandboxed machines (SSH sessions, containers, cloud coding agents) where a browser can't reach the CLI's localhost — unlike \`--non-interactive\`, which still requires opening the printed URL on the same machine as the CLI.
14032- \`meticulous agent js-coverage\` gained \`--latestForProject\`, which returns per-file coverage from the project's preferred latest successful test run.
14033
14034---
14035
14036### July 20, 2026 — list a project's recently recorded sessions
14037
14038- \`meticulous agent sessions\` **(new)** lists a project's most recently created sessions, newest first — useful, for instance, for finding the id of a session you just recorded.
14039- \`meticulous agent trigger-test-run\` gained \`--maxDurationSeconds\` (or \`none\` for unlimited) to override the replay engine's duration cap on runs with pinned \`--sessionIds\` — useful, for instance, to prevent agent-recorded sessions from being cut at the default 5min cap.
14040
14041\`\`\`bash
14042meticulous agent sessions
14043meticulous agent sessions --createdSince="2026-07-01" --createdUntil="2026-07-10"
14044meticulous agent sessions --recordedBy="[email protected]" --visitedUrlFilter="*/checkout*"
14045meticulous agent sessions --recordedSince 2026-07-10 --excludeSyntheticSessions --l
14045imit 10
14046meticulous agent trigger-test-run --sessionIds="<id1>,<id2>" --maxDurationSeconds=none
14047\`\`\`
14048
14049---
14050
14051### July 16, 2026 — MCP server for agents
14052
14053The Meticulous MCP server exposes the agent CLI's read and analysis commands as tools your coding agent can call directly — see the [MCP server](${o.AGENTS_MCP_SERVER_URL}) page for setup and the full list of available tools. Every read-only \`agent\` command has a matching tool; the two mutating ones, \`upload-build\` and \`trigger-test-run\`, aren't exposed yet but are coming soon.
14054
14055---
14056
14057### July 13, 2026 — review-state aware test-run-diffs, diff counts, and per-account default project
14058
14059- \`meticulous agent test-run-diffs\` now understands PR review state, and can report totals without the full list:
14060
14061  | Flag | What it does |
14062  |------|--------------|
14063  | \`--includeReviewDecisions\` | Add a \`decision\` column with each diff's PR review decision (\`accepted\`/\`rejected\`/\`ignored\`/\`unreviewed\`; \`unreviewed\` when undecided or there's no PR) |
14064  | \`--onlyUnreviewed\` | Return only the diffs still awaiting review — everything left to look at, across every difference (implies \`--includeAllDiffs\`, so the \`isSelected\` column is included) |
14065  | \`--counts\` | Print just the aggregate totals — number of replays, number of differences, and the review-decision breakdown (approved / ignored / rejected / unreviewed) — instead of the per-diff list |
14066
14067- Your default project is now a per-account setting, too:
14068  - \`meticulous auth set-project\` now persists your default project on your Meticulous account instead of a local file — so it's consistent across machines and available to the MCP server. \`meticulous auth logout\` leaves it untouched.
14069  - \`meticulous auth get-project\` **(new)** prints your default project, which you can also view and change from your user settings in the web app.
14070  - \`meticulous agent test-run-for-commit\`, \`test-run-diffs\`, \`js-coverage\`, and \`trigger-test-run\` gained \`--project\` — a one-off override (id, \`organization/name\` slug, or unique bare name) for that call only, which doesn't change your stored default.
14071
14072  \`\`\`bash
14073  meticulous auth set-project --project="my-org/my-project"
14074  meticulous auth get-project
14075  meticulous agent js-coverage --project="my-org/my-project"
14076  \`\`\`
14077
14078---
14079
14080### July 10, 2026 — test-run-diffs is differences-only
14081
14082\`meticulous agent test-run-diffs\` no longer returns matching screenshots — it reports only genuine visual differences, the same set Meticulous counts and displays everywhere else. The \`--includeMatches\` flag is removed.
14083
14084---
14085
14086### July 7, 2026 — get combined coverage from multiple test runs
14087
14088\`meticulous agent js-coverage\` gained \`--headPlusTestRunIds\` and \`--testRunIds\` for unioning coverage across several test runs (same project, same commit) — e.g. combining a run's head coverage with a separate run triggered via \`agent trigger-test-run --sessionIds="<ids>"\` to assess how these sessions improve coverage.
14089
14090\`\`\`bash
14091meticulous agent js-coverage --headPlusTestRunIds="<id1>,<id2>"
14092meticulous agent js-coverage --testRunIds="<id1>,<id2>,<id3>"
14093\`\`\`
14094
14095---
14096
14097### July 6, 2026 — consistent machine-readable output for agent & auth
14098
14099- \`agent\` and \`auth\` commands gained \`--json\` for JSON-structured output instead of default format.
14100- \`agent\` and \`auth\` commands also gained \`--verbose\`, which prints additional progress logs on stderr.
14101- \`--rawJson\` is renamed to \`--jsonArgs\` (old name still works, now deprecated).
14102
14103\`\`\`bash
14104meticulous agent js-coverage --json
14105meticulous auth whoami --json
14106\`\`\`
14107
14108---
14109
14110### July 1, 2026 — more coverage info, session pinning, and non-interactive login
14111
14112- \`meticulous agent js-coverage\` gained new flags for richer per-file coverage data: \`--includeExecutableRanges\`, \`--includeUncoveredRanges\`, \`--includeCoveragePercentage\`, and \`--prDiffOnly\` (test-run queries only), plus \`--includeAllFiles\` and \`--globFilter\` (also for replay and replay-diff queries).
14113- \`meticulous agent trigger-test-run\` can now run with no arguments at all — it infers the already-uploaded deployment for your local HEAD commit.
14114- \`meticulous agent trigger-test-run\` now accepts \`--sessionIds\`, a comma-separated list of session IDs to replay for both the base and the head, instead of the project's auto-selected golden set.
14115- \`meticulous agent trigger-test-run\` now also accepts \`--commitSha\` as an alternative to \`--deploymentId\`, resolving to the most recently uploaded deployment for that commit — useful for re-triggering a run against a commit that has already gone through Meticulous.
14116- \`meticulous auth login --non-interactive\` lets the login flow run without a TTY: it prints the login URL instead of opening a browser.
14117
14118\`\`\`bash
14119meticulous agent js-coverage --includeCoveragePercentage --prDiffOnly
14120meticulous agent trigger-test-run
14121meticulous agent trigger-test-run --deploymentId="<id>
14121" --baseSha="<base-sha>" --sessionIds="<id1>,<id2>"
14122meticulous agent trigger-test-run --commitSha="<sha>" --baseSha="<base-sha>"
14123meticulous auth login --non-interactive --project="my-org/my-project"
14124\`\`\`
14125
14126---
14127
14128### June 29, 2026 — separate build upload from triggering a test run
14129
14130Two new agent commands, \`upload-build\` and \`trigger-test-run\`, give agents their own counterparts to the CI upload commands (\`ci upload-assets\` / \`ci upload-container\`) — and split building and uploading your app from kicking off a test run. You can now upload a build once, capture its deployment ID, and trigger one or more runs against it independently. Git options such as the commit SHA are resolved automatically from your local repository.
14131
14132\`\`\`bash
14133# Upload a static build (or a container image) and capture the deploymentId
14134meticulous agent upload-build --appDirectory="<path-to-build>"
14135meticulous agent upload-build --localImageTag="<image-tag>"
14136
14137# Trigger a run against an uploaded build
14138meticulous agent trigger-test-run --deploymentId="<id>"
14139\`\`\`
14140
14141---
14142
14143### June 24, 2026 — smoother authentication and non-interactive project selection
14144
14145Authentication is easier to drive from scripts and agents, and a stored login is no longer shadowed by a stale token.
14146
14147- A logged-in OAuth session now takes precedence over a stale \`METICULOUS_API_TOKEN\` or \`~/.meticulous/config.json\` token, so you won't get silently stuck on an expired credential.
14148- \`meticulous auth login\` **(new)** forces a fresh browser login and then selects a project.
14149- \`meticulous auth whoami\` now also reports which credential is actually in use.
14150- \`meticulous auth logout\` now also clears the selected project, and warns if an environment-variable or config-file token will keep being used.
14151- \`meticulous auth list-projects\` **(new)** lists the projects you can access.
14152- Argument \`--project org/project\` **(new)** on \`login\` / \`set-project\` lets you select a project non-interactively.
14153
14154\`\`\`bash
14155meticulous auth login --project="my-org/my-project"
14156meticulous auth list-projects
14157\`\`\`
14158
14159---
14160
14161### June 19, 2026 — richer, curated test-run-diffs output
14162
14163By default, \`meticulous agent test-run-diffs\` now returns a curated, priority-ordered set of the most relevant visual differences as a single flat list. New flags let you control what comes back:
14164
14165| Flag | What it does |
14166|------|--------------|
14167| \`--includeDomDiffIds\` | Include DOM-diff IDs for each screenshot |
14168| \`--includeAllDiffs\` | Return every diff, not just the curated set (adds an \`isSelected\` column) |
14169| \`--includeMatches\` | Include matching and known-flaky screenshots too, not just differences (implies \`--includeAllDiffs\`) |
14170| \`--orderByReplayDiffs\` | Order by replay then event index instead of priority |
14171
14172Polling output is also quieter, and runs that can't produce diffs now fail fast with a clear message.
14173
14174---
14175
14176### June 12, 2026 — JavaScript coverage and lookup by commit
14177
14178New agent commands surface the JavaScript code coverage captured during replays, and let you resolve a test run straight from a commit — so an agent can go from local git context to the right run without tracking run IDs.
14179
14180\`\`\`bash
14181meticulous agent js-coverage          # coverage for a test run (defaults to the current git HEAD)
14182meticulous agent js-coverage-diff     # base-vs-head coverage diff for a replay diff
14183meticulous agent test-run-for-commit  # the latest test run for the current commit
14184\`\`\`
14185`},{id:"agents-cli-commands",url:"/docs/agents/cli-commands",document:`---
14186{
14187  "title": "CLI commands for agents"
14188}
14189---
14190
14191# {% $frontmatter.title %}
14192
14193The Meticulous CLI provides tools which enable agents to interface with Meticulous: get test run diffs, replay details, coverage, and more. We furthermore provide [agent skills](${o.AGENTS_SKILLS_URL}) which compose these tools into higher-level workflows, like a skill to review a PR test run.
14194
14195- [Install & update the Meticulous CLI](#install-update-the-meticulous-cli)
14196- [Authentication](#authentication)
14197- [Command reference](#command-reference)
14198
14199---
14200
14201## Install & update the Meticulous CLI
14202
14203To install, and update, the Meticulous CLI:
14204
14205\`\`\`bash
14206npm install --global @alwaysmeticulous/cli@latest
14207\`\`\`
14208
14209You can also install it locally per-project instead of globally.
14210
14211The CLI is under active development with frequent changes and improvements — re-run the same command to update the CLI to the latest version.
14212
14213---
14214
14215## Authentication
14216
14217Authenticate the CLI with your Meticulous account:
14218
14219\`\`\`bash
14220meticulous auth login
14221\`\`\`
14222
14223This will open a browser to sign in, and if you're a member of multiple projects, let you pick a default project to use. This default is saved to your Meticulous account — so it's consistent across machines and available to the MCP server — and you can change it any time from your [user settings](${o.USER_SETTINGS_U
14223RL}) or by running \`meticulous auth set-project\`. Non-interactive versions of both commands are also available, for use in CI or other non-TTY environments:
14224
14225\`\`\`bash
14226meticulous auth login --non-interactive --project <organization/project>
14227meticulous auth set-project --project <organization/project>
14228\`\`\`
14229
14230\`set-project\` is fully unattended. \`login --non-interactive\` still completes via a localhost callback, so it only drops the requirement for an interactive TTY — the URL it prints must still be opened on this same machine.
14231
14232If the CLI is running on a remote or sandboxed machine (SSH session, container, cloud coding agent) where a browser can't reach that machine's localhost, use \`--device\` instead: it logs in via the OAuth device flow, printing a URL and code that can be opened and confirmed in a browser on any device.
14233
14234\`\`\`bash
14235meticulous auth login --device --project <organization/project>
14236\`\`\`
14237
14238{% expand title="Alternative: using an API token" %}
14239Select the project below that contains the sessions you wish to work with, then copy the token:
14240
14241{% code_with_project_selector %}
14242METICULOUS_API_TOKEN:
14243{% standalone_api_token /%}
14244{% /code_with_project_selector %}
14245
14246*Be very careful with this API token, since it allows the holder access to your recorded sessions.*
14247
14248Pass it to the CLI in one of two ways:
14249
14250- Set the \`METICULOUS_API_TOKEN\` environment variable:
14251
14252  \`\`\`bash
14253  export METICULOUS_API_TOKEN="<paste-token-here>"
14254  \`\`\`
14255
14256- Or store it in \`~/.meticulous/config.json\`:
14257
14258  \`\`\`json
14259  { "apiToken": "<paste-token-here>" }
14260  \`\`\`
14261{% /expand %}
14262
14263---
14264
14265## Command reference
14266
14267### Global command options
14268
14269\`\`\`bash
14270meticulous <command> --json               # output in JSON format on stdout instead of default format
14271meticulous <command> --jsonArgs="<json>"  # pass all options as a JSON string
14272meticulous <command> --verbose            # also print progress to stderr (instead of just warnings)
14273meticulous <command> --dryRun             # print what a mutating command would do, without doing it
14274\`\`\`
14275
14276---
14277
14278### Authentication
14279
14280\`\`\`bash
14281meticulous auth login          # force a fresh browser login, then select a project (interactive)
14282meticulous auth login --non-interactive --project="{org}/{proj}"  # same, but non-interactive
14283meticulous auth login --device --project="{org}/{proj}"  # same, but OAuth device flow
14284meticulous auth whoami         # show how you're currently authenticated
14285meticulous auth logout         # revoke and clear stored tokens
14286meticulous auth get-project    # print your default project
14287meticulous auth set-project    # choose your default project (interactive)
14288meticulous auth set-project --project="{org}/{proj}"  # same, but non-interactive
14289meticulous auth list-projects  # list the projects you can access
14290\`\`\`
14291
14292---
14293
14294### Discover the CLI surface
14295
14296\`\`\`bash
14297meticulous schema           # full schema
14298meticulous schema simulate  # narrow to a single command or group
14299\`\`\`
14300
14301---
14302
14303### Look up the test run for a commit
14304
14305\`\`\`bash
14306meticulous agent test-run-for-commit                      # latest run for the current git HEAD
14307meticulous agent test-run-for-commit --commitSha="<sha>"  # …or for a specific commit
14308meticulous agent test-run-for-commit --prNumber=123       # …or for a pull request's head commit
14309meticulous agent test-run-for-commit --dontWaitForTestRunToComplete  # don't block on in-progress runs
14310meticulous agent test-run-for-commit --project="{org}/{proj}"  # override default project for this call
14311\`\`\`
14312
14313---
14314
14315### List a project's test runs
14316
14317\`\`\`bash
14318meticulous agent test-runs                              # 100 most recent pull request test runs
14319meticulous agent test-runs --prNumber=123               # test runs of one pull request
14320meticulous agent test-runs --baseTestRuns               # base test runs instead of pull request test runs
14321meticulous agent test-runs --latestPerPullRequest       # newest run of each pull request
14322meticulous agent test-runs --status=Running,Scheduled   # only runs in these statuses
14323meticulous agent test-runs --withDiffsOnly              # only runs that found differences
14324meticulous agent test-runs --withCheckIssuesOnly        # only runs where a builtin check failed or warned
14325meticulous agent test-runs --withCheckIssuesOnly --checkIds=accessibility  # …only for these checks
14326meticulous agent test-runs --createdSince="2026-09-01"  # only runs created at/after this date
14327meticulous agent test-runs --includeBaseTestRunId       # add a baseTestRunId column
14328meticulous agent test-runs --includeDiffCount           # add a diffCount column
14329meticulous agent test-runs --includeCheckIssueCounts    # add checkWarningCount and checkFailureCount columns
14330meticulous agent test-runs --includeDurationSeconds     # add a durationSeconds column
14331meticulous agent test-runs --project="{org}/{proj}"     # override default project for this call
14332meticulous agent test-runs --limit=25 --offset=50       # override count / page through results
14333\`\`\`
14334
14335---
14336
14337### Retrieve diffs for a test run
14338
14339\`\`\`bash
14340meticulous agent test-run-diffs                       # diffs for test run on current commit
14341meticulous agent test-run-diffs --testRunId="<id>"    # …or for an explicit test run
14342meticulous agent test-run-diffs --commitSha="<sha>"   # …or for a specific commit
14343meticulous agent test-run-diffs --prNumber=123        # …or for a pull request's head commit
14344meticulous agent test-run-diffs --dontWaitForTestRunToComplete  # don't block on in-progress runs
14345meticulous agent test-run-diffs --includeReplayIds    # include base and head replay IDs per diff
14346meticulous agent test-run-diffs --includeMismatchFraction  # include the fraction of changed pixels
14347meticulous agent test-run-diffs --includeReviews        # add decision and comment count per diff
14348meticulous agent test-run-diffs --includeDomDiffIds   # include DOM-diff IDs per screenshot
14349meticulous agent test-run-diffs --includeSimilarGroupId  # add the Similar group id per diff
14350meticulous agent test-run-diffs --similarGroupId="<id>"  # every diff in a Similar group
14351meticulous agent test-run-diffs --includeAllDiffs     # every diff, not just the selected set
14352meticulous agent test-run-diffs --onlyUnreviewed      # only diffs awaiting review
14353meticulous agent test-run-diffs --onlyRejected        # all rejected diffs
14354meticulous agent test-run-diffs --onlyWithComments    # only diffs with open review comments
14355meticulous agent test-run-diffs --orderByReplayDiffs  # group by replay diff instead of priority order
14356meticulous agent test-run-diffs --project="{org}/{proj}"  # override default project for this call
14357meticulous agent test-run-diffs --counts              # just the total counts, not the full diff list
14358\`\`\`
14359
14360---
14361
14362### Investigate a diff in more detail
14363
14364\`\`\`bash
14365# Download screenshots to ~/.meticulous/agent-images/, or get image URLs
14366meticulous agent image-files --replayDiffId="<id>" --screenshotName="<name>"
14367meticulous agent image-urls --replayDiffId="<id>" --screenshotName="<name>"
14368
14369# DOM diff for a single replay-diff screenshot
14370meticulous agent dom-diff --replayDiffId="<id>" --screenshotName="<name>"
14371
14372# Timeline diff for a replay diff
14373meticulous agent timeline-diff --replayDiffId="<id>"
14374\`\`\`
14375
14376---
14377
14378### Review diffs
14379
14380\`\`\`bash
14381meticulous agent diff-comments --replayDiffId="<id>" --screenshotName="<name>"
14382meticulous agent diff-comments ... --includeResolved  # include resolved comments
14383meticulous agent approve-diff --replayDiffId="<id>" --screenshotName="<name>"
14384meticulous agent reject-diff ... --reason="..." --x=0.5 --y=0.5
14385meticulous agent ignore-diff ... --reason="..." --x=0.5 --y=0.5
14386meticulous agent create-diff-comment ... --text="..." --x=0.4 --y=0.6
14387meticulous agent reply-to-diff-comment --commentId="<id>" --text="..."
14388\`\`\`
14389
14390---
14391
14392### Retrieve a non-visual check report
14393
14394\`\`\`bash
14395meticulous agent test-run-check --checkId="accessibility"  # builtin check for the current commit
14396meticulous agent test-run-check --checkId="network-requests" --testRunId="<id>"  # …or an explicit run
14397meticulous agent test-run-check --checkId="accessibility" --commitSha="<sha>"  # …or a specific commit
14398meticulous agent test-run-check --checkId="accessibility" --prNumber=123  # …or for a pull request's head commit
14399meticulous agent test-run-check --checkType="custom" --checkId="my-check"  # customer-reported check
14400
14401# List available check IDs
14402meticulous agent test-run-check --availableIds  # for the current commit
14403meticulous agent test-run-check --availableIds --testRunId="<id>"  # …or an explicit run
14404meticulous agent test-run-check --availableIds --commitSha="<sha>"  # …or a specific commit
14405meticulous agent test-run-check --availableIds --prNumber=123  # …or for a pull request's head commit
14406\`\`\`
14407
14408---
14409
14410### Review non-visual checks
14411
14412\`\`\`bash
14413meticulous agent check-comments --testRunId="<id>" --checkId="network-requests"
14414meticulous agent check-comments ... --includeResolved  # include reasons a later decision replaced
14415meticulous agent approve-check --testRunId="<id>" --checkId="network-requests"
14416meticulous agent approve-check --prNumber=123 --checkId="network-requests"  # …or for a pull request's head commit
14417meticulous agent reject-check ... --reason="..."
14418meticulous agent ignore-check ... --reason="..."
14419meticulous agent reject-check --testRunId="<id>" --checkType="custom" --checkId="my-check" --reason="..."  # customer-reported check
14420\`\`\`
14421
14422---
14423
14424### Inspect JS code coverage
14425
14426\`\`\`bash
14427# Per-file coverage: a test run, the project's latest run, or a single replay
14428meticulous agent js-coverage                           # coverage for current commit
14429meticulous agent js-coverage --testRunId="<id>"        # …or for an explicit test run
14430meticulous agent js-coverage --commitSha="<sha>"       # …or for a specific commit
14431meticulous agent js-coverage --prNumber=123            # …or for a pull request's head commit
14432meticulous agent js-coverage --dontWaitForTestRunToComplete  # don't block on in-progress runs
14433meticulous agent js-coverage --latestForProject        # …or project's preferred latest successful run
14434meticulous agent js-coverage --project="{org}/{proj}"  # override default project for this call
14435meticulous agent js-coverage --replayId="<id>"         # coverage for a single replay
14436meticulous agent js-coverage --replayId="<id>" --screenshotName="<name>"  # or a single screenshot
14437meticulous agent js-coverage --summary                 # aggregate totals, not the per-file list
14438
14439# Coverage diff: a whole test run against its own base run, or one replay diff
14440meticulous agent js-coverage-diff                        # diff for the current commit's run
14441meticulous agent js-coverage-diff --testRunId="<id>"     # …or for an explicit test run
14442meticulous agent js-coverage-diff --commitSha="<sha>"    # …or for a specific commit
14443meticulous agent js-coverage-diff --prNumber=123         # …or for a pull request's head commit
14444meticulous agent js-coverage-diff --replayDiffId="<id>"  # …or the diff of a single replay pair
14445meticulous agent js-coverage-diff --replayDiffId="<id>" --screenshotName="<name>"
14446meticulous agent js-coverage-diff --summary              # aggregate difference (whole-run only)
14447
14448# Filter and page — these work the same on js-coverage and js-coverage-diff
14449meticulous agent js-coverage --globFilter="src/components/**"  # only matching repo paths
14450meticulous agent js-coverage --globFilter="src/**" --globFilter="libs/**"  # any of several
14451meticulous agent js-coverage --limit=50                        # page size (1-1000, default 100)
14452meticulous agent js-coverage --limit=100 --offset=100          # the next page
14453
14454# Order, choose columns, and pick rows (js-coverage on a whole test run only)
14455meticulous agent js-coverage --orderBy=uncoveredLines --limit=50  # the 50 least-covered files
14456meticulous agent js-coverage --orderBy=coveragePercentage --order=asc
14457meticulous agent js-coverage --includeExecutedRanges      # executed line ranges (default)
14458meticulous agent js-coverage --includeExecutableRanges    # line ranges that could be executed
14459meticulous agent js-coverage --includeUncoveredRanges     # executable ranges that were not executed
14460meticulous agent js-coverage --includeLineCounts          # executed/executable/uncovered line counts
14461meticulous agent js-coverage --includeCoveragePercentage  # % of executable lines executed
14462meticulous agent js-coverage --includeAllFiles            # not just ones with coverage
14463meticulous agent js-coverage --prDiffOnly                 # restrict to files changed in the PR diff
14464
14465# Aggregated coverage for multiple test runs (same project + commit; test-run only)
14466meticulous agent js-coverage --headPlusTestRunIds="<id1>,<id2>"  # in addition to the current commit
14467meticulous agent js-coverage --testRunIds="<id1>,<id2>,<id3>"    # list of runs to combine
14468
14469# Complete a base run
14470meticulous agent complete-base-run                        # replay the rest of the current commit's base run
14471meticulous agent complete-base-run --testRunId="<id>"     # …or of an explicit run
14472meticulous agent complete-base-run --commitSha="<sha>"    # …or of a specific commit's run
14473meticulous agent complete-base-run --project="{org}/{proj}"  # override default project for this call
14474meticulous agent complete-base-run --dontWaitForTestRunToComplete  # return once the replays are scheduled
14475
14476# Add sessions a pinned-session run (trigger-test-run --sessionIds) replayed to the selected set
14477meticulous agent promote-sessions --testRunId="<id>"  # all of the run's sessions
14478meticulous agent promote-sessions --testRunId="<id>" --sessionIds="<id1>"  # …or a subset of them
14479\`\`\`
14480
14481---
14482
14483### Find recently created sessions
14484
14485\`\`\`bash
14486meticulous agent sessions                               # 100 most recently created sessions
14487meticulous agent sessions --project="{org}/{proj}"      # override default project for this call
14488meticulous agent sessions --createdSince="2026-07-01"   # only sessions created at/after this date
14489meticulous agent sessions --recordedSince="2026-07-01"  # only sessions originally recorded at/after
14490meticulous agent sessions --recordedBy="[email protected]"  # only sessions recorded by this identity
14491meticulous agent sessions --excludeSyntheticSessions    # drop patched/sliced/mutated sessions
14492meticulous agent sessions --excludeAgentReviewSessions  # drop sessions recorded by Agent swarm
14493meticulous agent sessions --requireInitialNavigationResponse  # only sessions with a recorded HTML document
14494meticulous agent sessions --visitedUrlFilter="*/checkout*"  # only sessions that visited a matching URL
14495meticulous agent sessions --selectedSet                 # only sessions in the current selected set
14496meticulous agent sessions --selectedSet="2026-07-01"    # only sessions selected as of that point
14497meticulous agent sessions --selectedSet --includeSelectedSince  # add when each entered the set
14498meticulous agent sessions --selectedSet --orderBy=rank  # in the selection's own pick order
14499meticulous agent sessions --selectedSet --orderBy=selectedSince  # most recently selected first
14500meticulous agent sessions --selectedSet --includeAdditionalCoverage  # add the coverage each one added
14501meticulous agent sessions --selectedSet --orderBy=additionalCoverage  # biggest contributors first
14502meticulous agent sessions --order=asc                   # reverse the chosen ordering
14503meticulous agent sessions --includeDurationSeconds      # add a durationSeconds column
14504meticulous agent sessions --includeNumberUserEvents     # add a numberUserEvents column
14505meticulous agent sessions --includeNumberUrlsVisited    # add a numberUrlsVisited column
14506meticulous agent sessions --includeStartUrl             # add a startUrl column
14507meticulous agent sessions --includeAbandonedReason      # add an abandonedReason column
14508meticulous agent sessions --includeSource               # add a source column
14509meticulous agent sessions --limit=25 --offset=50        # override count / page through results
14510
14511# Identify sessions that exercise your branch's code changes
14512meticulous local relevant-sessions --format=multi-file --minimum-times-to-cover-each-line=1
14513\`\`\`
14514
14515---
14516
14517### Export reporting statistics
14518
14519\`\`\`bash
14520meticulous agent test-run-stats --since="2026-08-01"        # --until defaults to now
14521meticulous agent test-run-stats --testRunIds="<id1>,<id2>"  # or scope by identifiers instead
14522meticulous agent test-run-stats --prNumbers="123,456"
14523meticulous agent test-run-stats --commitShas="<sha1>,<sha2>"
14524meticulous agent project-daily-stats --since="2026-08-01"   # matches the Metrics dashboard
14525meticulous agent test-run-event-stats --since="2026-08-01"  # views, reviews, comments
14526meticulous agent test-run-event-stats --eventTypes="test_run_viewed" --since="2026-08-01"
14527meticulous agent test-run-event-stats --cursor="<next-cursor>" --since="2026-08-01"  # next page
14528\`\`\`
14529
14530---
14531
14532### Upload a build and trigger a test run
14533
14534\`\`\`bash
14535# Upload a static build, or a container image, and capture the deploymentId
14536meticulous agent upload-build --appDirectory="<path-to-build>"
14537meticulous agent upload-build --localImageTag="<image-tag>" --commitSha="<sha>"
14538
14539# Trigger a run against that deployment (infers a diff against local HEAD)
14540meticulous agent trigger-test-run --deploymentId="<id>"
14541meticulous agent trigger-test-run --project="{org}/{proj}"  # override default project for this call
14542
14543# …and pin an explicit base (diffs against local HEAD)
14544meticulous agent trigger-test-run --deploymentId="<id>" --baseSha="<sha>"
14545
14546# …or pass a diff yourself, instead of inferring one locally
14547meticulous agent trigger-test-run --deploymentId="<id>" --baseSha="<sha>" --gitDiffOutput="<diff>"
14548
14549# …or skip the upload step and target an already-uploaded deployment for a commit
14550meticulous agent trigger-test-run
14551meticulous agent trigger-test-run --commitSha="<sha>"
14552
14553# Replay only specific sessions, instead of the auto-selected golden set
14554meticulous agent trigger-test-run --sessionIds="<id1>,<id2>"
14555\`\`\`
14556
14557---
14558
14559### Submit feedback about Meticulous
14560
14561\`\`\`bash
14562# Tell the Meticulous team whether Meticulous helped, and what would have made your task easier
14563meticulous agent submit-feedback --message="<one or two sentences>"
14564meticulous agent submit-feedback --message="<…>" --outcome="helped"       # or "neutral" / "hindered"
14565meticulous agent submit-feedback --message="<…>" --testRunId="<id>"       # tie it to a test run
14566meticulous agent submit-feedback --message="<…>" --skill="meticulous-review"  # workflow being followed
14567meticulous agent submit-feedback --message="<…>" --agentName="claude-code" --agentModel="<model>"
14568\`\`\`
14569
14570---
14571
14572### Set up an AI-ready debug workspace
14573
14574\`\`\`bash
14575# Download all replay data into a structured local debug workspace in ~/.meticulous
14576meticulous debug replay <replayId>           # debug a single replay, optionally with --baseReplayId
14577meticulous debug replay-diff <replayDiffId>  # debug a specific replay diff
14578meticulous debug clean                       # clean up old debug workspaces
14579\`\`\`
14580
14581---
14582
14583### Run Agent swarm (beta)
14584
14585Agent swarm uploads a build and lets a Meticulous-hosted agent explore and test it. The Agent swarm page reports the test-case results and screenshots, and the discovered flows are saved as additional sessions for the PR. This is an opt-in beta: your project must be enabled before \`ci agent-test\` can launch.
14586
14587\`\`\`bash
14588meticulous ci agent-test \\
14589  --assetsDir="build" \\
14590  --commitSha="<pr-head-sha>"
14591\`\`\`
14592
14593By default, the agent reads instructions from \`.meticulous/agent-swarm-instructions.md\` in the repository at the commit being tested. Use \`--instructionsFile\` to override those instructions for one CLI-launched run.
14594
14595Use exactly one of \`--assetsDir\` or \`--localImageTag\`. For a staging backend, add \`--backendUrl\`; \`--backendProxyPaths\` defaults to \`/api\`. Contact Meticulous to configure egress policies for any absolute cross-origin API or auth requests. Uploaded assets are served at \`http://localhost:8000\` by default; pass \`--appPort\` to override. Do not combine \`--backendUrl\` with \`--enableLocalMocks\`.
14596
14597For a container build, use \`--containerPort\`, repeatable \`--containerEnv NAME=value\` options, and \`--containerHealthCheckEndpoint\` when the defaults do not fit your image. Use \`--enableLocalMocks\` to mock the container's network traffic from relevant recorded sessions. See [Set up Agent swarm](${o.AGENT_SWARM_DOCS_URL}) for the prerequisites, CI workflow, and staging-backend setup.
14598
14599---
14600
14601### Replay a single session locally
14602
14603\`\`\`bash
14604meticulous simulate --sessionId="<id>" --appUrl="<appUrl>"
14605meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --baseReplayId="<id>"  # diff against base
14606meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --screenshot  # capture screenshots
14607meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --headless    # run in headless mode
14608meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --devtools    # open Chromium DevTools
14609meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --maxDurationMs=<ms>  # set max virtual time
14610\`\`\`
14611
14612---
14613
14614### Download artefacts
14615
14616\`\`\`bash
14617# Downloads to ~/.meticulous/ by default (override with --dataDir)
14618meticulous download session --sessionId="<id>"
14619meticulous download replay --replayId="<id>"
14620meticulous download test-run --testRunId="<id>"
14621\`\`\`
14622`},{id:"agents-mcp-server",url:"/docs/agents/mcp-server",document:`---
14623{
14624  "title": "MCP server for agents"
14625}
14626---
14627
14628# {% $frontmatter.title %}
14629
14630The Meticulous MCP server provides tools which enable agents to interface with Meticulous: get test run diffs, replay details, coverage, and more. It's a hosted [Model Context Protocol](https://modelcontextprotocol.io) endpoint, hosted at https://app.meticulous.ai/api/mcp. We furthermore provide [agent skills](${o.AGENTS_SKILLS_URL}) which compose these tools into higher-level workflows, like a skill to review a PR test run.
14631
14632- [What it's for](#what-its-for)
14633- [Connecting a client](#connecting-a-client)
14634- [Authenticating with a token instead](#authenticating-with-a-token-instead)
14635- [Available tools](#available-tools)
14636- [What this connector can access](#what-this-connector-can-access)
14637- [Troubleshooting](#troubleshooting)
14638- [Support and policies](#support-and-policies)
14639
14640---
14641
14642## What it's for
14643
14644Meticulous records real user sessions in your app and replays them against every commit to catch visual regressions before they ship. This server lets an agent — reviewing a PR, debugging a failing check, or investigating coverage — pull that data directly: which screenshots changed and why, whether a diff is a real regression or noise, which lines of a change are covered by a test, and (for CI/build agents) trigger a new run against a build.
14645
14646---
14647
14648## Connecting a client
14649
14650The server is an OAuth 2.1 protected resource — it supports dynamic client registration and standard discovery (\`/.well-known/oauth-protected-resource\`, \`/.well-known/oauth-authorization-server\`), so clients connect with just the endpoint URL above. There is no client id, secret, scope, or authorization-server URL to configure by hand. Login happens in your browser on first connect and tokens refresh automatically.
14651
14652{% expand title="OAuth details for other clients" %}
14653
14654Discovery follows the [MCP authorization spec](https://modelcontextprotocol.io/specification/draft/basic/authorization). The protected-resource document at \`https://app.meticulous.ai/.well-known/oauth-protected-resource\` names the authorization server, whose metadata is at \`https://app.meticulous.ai/.well-known/oauth-authorization-server/auth/realms/meticulous\` (also served at the OpenID Connect location \`/.well-known/openid-configuration/auth/realms/meticulous\`). If your client lets you set the authorization-server metadata URL directly, use that one.
14655
14656- **Dynamic client registration** is at the \`registration_endpoint\` in that metadata and returns a public client. The identity provider's own registration endpoint is intentionally closed.
14657- **Without registration:** client id \`meticulous-cli\`, public client with PKCE (S256), scopes \`openid email profile offline_access\`.
14658- **Request \`offline_access\` explicitly.** It is an optional scope, so it is granted only when your authorization request asks for it, and it is what grants the long-lived refresh token. Omit it and the access token is short-lived and tied to the browser SSO session, leaving the client no way to refresh and no choice but to send the user back through an interactive login.
14659
14660{% /expand %}
14661
14662**Claude Code**
14663
14664Run the following in your terminal:
14665
14666\`\`\`bash
14667claude mcp add --transport http Meticulous https://app.meticulous.ai/api/mcp
14668\`\`\`
14669
14670Then, in Claude Code, type \`/mcp\` and choose "Authenticate" for the Meticulous MCP.
14671
14672**Cursor**
14673
14674Add to \`~/.cursor/mcp.json\` (global) or \`.cursor/mcp.json\` (per project):
14675
14676\`\`\`json
14677{
14678  "mcpServers": {
14679    "Meticulous": { "url": "https://app.meticulous.ai/api/mcp" }
14680  }
14681}
14682\`\`\`
14683
14684**Codex/ChatGPT**
14685
14686Add a server with name "Meticulous" and URL \`https://app.meticulous.ai/api/mcp\`, then click "Authenticate".
14687
14688Calls are scoped to your default project. If you only have access to a single project, that one is used automatically; otherwise set a default in your [user settings](${o.USER_SETTINGS_URL}) in the web app, or via the CLI: \`meticulous auth set-project\`.
14689
14690---
14691
14692## Authenticating with a token instead
14693
14694Any MCP client can also authenticate with a static bearer token instead of the browser OAuth flow — useful in CI, or wherever an interactive login isn't possible. The token is a Meticulous OAuth token or a project API token (see [Setup > Org-wide setup](${o.AGENTS_SETUP_URL}#org-wide-setup) for how to obtain one). A project API token scopes every call to that one project.
14695
14696\`\`\`json
14697{
14698  "url": "https://app.meticulous.ai/api/mcp",
14699  "headers": { "Authorization": "Bearer <token>" }
14700}
14701\`\`\`
14702
14703---
14704
14705## Available tools
14706
14707Every tool takes broadly the same arguments as its matching [CLI command](${o.AGENTS_CLI_COMMANDS_URL}) and returns the same data as that command's \`--json\` output — with minor differences inherent to a hosted endpoint rather than a local CLI (for example, no git-inferred \`commitSha\`, since the server has no checkout to infer it from). "Access" below reflects each tool's MCP annotations, which determine whether a client can call it without per-call confirmation.
14708
14709### Identity and project selection
14710
14711| Tool | Access | What it does |
14712|---|---|---|
14713| \`whoami\` | Read | Show the identity the connection is authenticated as, and the project it resolves to. |
14714| \`list_projects\` | Read | List the projects the authenticated user, or API token, can access. |
14715| \`get_project\` | Read | Show the project project-scoped tools use when not given a \`project\` argument, and where that came from. |
14716| \`set_project\` | Write | Change the default project for the user account — every session and machine, not just this connection. |
14717
14718### Test runs and diffs
14719
14720| Tool | Access | What it does |
14721|---|---|---|
14722| \`get_test_run_for_commit\` | Read | Look up the latest test run for a commit or pull request; returns its ID and status. |
14723| \`get_test_runs\` | Read | List a project's pull request test runs, or its base test runs, newest first, optionally for one pull request. |
14724| \`get_test_run_diffs\` | Read | Get the (curated, full, or Similar-group) list of screenshot diffs for a test run. |
14725| \`get_test_run_diffs_counts\` | Read | Get aggregate diff counts for a test run, including the six-way review-decision breakdown. |
14726| \`get_image_urls\` | Read | Get signed URLs for a screenshot diff's before/after/diff images. |
14727| \`get_images\` | Read | Get a screenshot diff's before, after, and diff images as native MCP image blocks. |
14728| \`get_dom_diff\` | Read | Get the structural DOM diff for one screenshot diff, as unified-diff-style hunks. |
14729| \`get_timeline_diff\` | Read | Get the list of timeline event differences (e.g. network requests, DOM mutations) for a replay diff. |
14730| \`get_test_run_check\` | Read | Get the Markdown report for a builtin or custom non-visual check. |
14731| \`get_test_run_check_available_ids\` | Read | List the check IDs available for a test run, for the checks that have reported results so far. |
14732
14733#### Results that are not ready yet
14734
14735Every tool that reads a test run's or replay's results — \`get_test_run_diffs\`, \`get_test_run_diffs_counts\`, \`get_test_run_check\`, the four test-run coverage tools, and the replay-level \`get_replay_js_coverage\` and \`get_replay_diff_js_coverage_diff\` — returns \`{ status: 'processing' }\` (the diffs list also \`{ status: 'pending' }\`) while the test run or replay is still running, or while its result is still being computed after it finished, rather than an empty or partial result that would read as a finished one. Callers should poll the same tool every 10s until it returns the result, for up to 10 minutes. A \`failed\` result (with a \`reason\`) means nothing is still computing and retrying reproduces it.
14736
14737For \`get_test_run_diffs\`, a poll can itself start the underlying compute workflow; that lazy compute does not make the tool a write. A completed \`get_test_run_check\` response is \`{ status: 'complete', text }\`, or \`{ status: 'complete', text, url }\` when the report is too large to return inline — \`text\` is then a short notice and the full report is downloadable from \`url\`. With \`checkType: 'custom'\`, an error saying the run is not expecting custom check results can be transient shortly after the run completes, since your CI registers its checks separately from the run itself — this is usually resolved by retrying for a minute or so. Use \`get_test_run_check_available_ids\` to find a valid \`checkId\` instead of guessing one.
14738
14739### Reviewing diffs
14740
14741| Tool | Access | What it does |
14742|---|---|---|
14743| \`get_diff_comments\` | Read | Get the review comments (with replies) for a screenshot diff, oldest first. |
14744| \`approve_diff\` | Write | Agent-approve a screenshot diff, optionally commenting why, if the project allows it. |
14745| \`reject_diff\` | Write | Agent-reject a screenshot diff and comment why. |
14746| \`ignore_diff\` | Write | Agent-ignore a screenshot diff as unrelated to the change under review, and comment why; a real ignore only if the project allows it. |
14747| \`create_diff_comment\` | Write | Start a review comment thread at approximate image coordinates. |
14748| \`reply_to_diff_comment\` | Write | Reply to an existing review comment thread. |
14749
14750### Reviewing non-visual checks
14751
14752| Tool | Access | What it does |
14753|---|---|---|
14754| \`get_check_comments\` | Read | Get the reasons recorded with agent decisions on a non-visual check of a test run, oldest first. |
14755| \`approve_check\` | Write | Agent-approve a failing non-visual check, optionally with a reason, if the project allows it. |
14756| \`reject_check\` | Write | Agent-reject a failing non-visual check, with a reason. |
14757| \`ignore_check\` | Write | Agent-ignore a failing non-visual check as unrelated to the change under review, with a reason, if the project allows it. |
14758
14759### JS coverage
14760
14761| Tool | Access | What it does |
14762|---|---|---|
14763| \`get_test_run_js_coverage\` | Read | Get per-file JavaScript coverage for a test run. |
14764| \`get_project_js_coverage\` | Read | Get per-file JavaScript coverage for a project's latest successful test run. |
14765| \`get_test_run_js_coverage_summary\` | Read | Get aggregate JavaScript coverage totals for a test run. |
14766| \`get_project_js_coverage_summary\` | Read | Get aggregate JavaScript coverage totals for a project's latest successful test run. |
14767| \`get_replay_js_coverage\` | Read | Get per-file JavaScript coverage for a single replay, or one screenshot of it. |
14768| \`get_test_run_js_coverage_diff\` | Read | Get per-file JavaScript coverage differences for a test run against its own base run. |
14769| \`get_test_run_js_coverage_diff_summary\` | Read | Get the aggregate JavaScript coverage difference for a test run against its own base run. |
14770| \`get_replay_diff_js_coverage_diff\` | Read | Get per-file JavaScript coverage differences (base vs. head) for a replay diff. |
14771
14772### Sessions
14773
14774| Tool | Access | What it does |
14775|---|---|---|
14776| \`get_sessions\` | Read | List a project's recorded sessions, newest first by default, optionally narrowed to the selected set. |
14777| \`get_session_data\` | Read | Get the recorded user-flow and network summary for a session — useful for understanding what a replay exercises. |
14778
14779### Reporting statistics
14780
14781| Tool | Access | What it does |
14782|---|---|---|
14783| \`get_test_run_stats\` | Read | Export reporting statistics for a project's test runs. |
14784| \`get_project_daily_stats\` | Read | Export daily project reporting statistics matching the Metrics dashboard. |
14785| \`get_test_run_event_stats\` | Read | Export a project's test-run reporting events such as views, reviews, and comments. |
14786
14787### Triggering a test run
14788
14789| Tool | Access | What it does |
14790|---|---|---|
14791| \`request_asset_upload\` | Write | Request a signed URL to upload a zipped static-asset build. |
14792| \`register_asset_build\` | Write | Register an uploaded zipped asset build as an ephemeral deployment. Keep the returned deploymentId; local uploads are not discoverable later by commit SHA. |
14793| \`request_container_upload\` | Write | Request registry credentials to push a Docker container build. |
14794| \`register_container_build\` | Write | Register a pushed container build as an ephemeral deployment. Keep the returned deploymentId; local uploads are not discoverable later by commit SHA. |
14795| \`trigger_test_run\` | Write | Trigger a test run against a registered deploymentId, or a persistent CI deployment identified by commit SHA. |
14796| \`complete_base_run\` | Write | Replay the selected sessions a base run has not run yet. |
14797| \`promote_sessions\` | Write | Add sessions a pinned-session test run replayed to the project's selected set, without waiting for session selection. |
14798
14799### Feedback
14800
14801| Tool | Access | What it does |
14802|---|---|---|
14803| \`submit_feedback\` | Write | Send free-form feedback about Meticulous to the Meticulous team. |
14804
14805---
14806
14807## What this connector can access
14808
14809- **Reads:** your identity and the projects you can access; test runs and their status; reporting statistics and engagement events; screenshot diffs, outcomes, and images; DOM and timeline diffs; JavaScript coverage; and recorded session data (the user flow and network requests a session exercises).
14810- **Writes** (only when the corresponding tool is called): uploads a build's assets or container image, registers it as a deployment, triggers a new test run against it, changes your account's default project, and submits feedback to the Meticulous team.
14811- **Never accesses:** your repository's source code, or PR titles/descriptions — Meticulous's diffs and coverage are computed from screenshots, DOM snapshots, and instrumented JS execution, not from reading your co
14811de.
14812- **Scope:** every call is scoped to the projects the authenticated user (or, for a project API token, the single owning project) has access to.
14813
14814---
14815
14816## Troubleshooting
14817
14818- **401/403 on every call:** your token has expired or was revoked — reconnect via \`/mcp\` (Claude Code) or your client's equivalent to re-authenticate.
14819- **Claude Code: "Issuer mismatch in authorization response (RFC 9207)":** Claude Code is reusing an authorization-server URL it cached from an earlier login. In \`/mcp\`, pick the Meticulous server and choose **Clear authentication** (not Re-authenticate), then **Authenticate** again.
14820- **"No default project" errors:** call \`set_project\` (\`list_projects\` shows the options), set one from your [user settings](${o.USER_SETTINGS_URL}) in the web app, or run \`meticulous auth set-project\` — or, for a one-off call, pass the optional \`project\` argument most tools accept (id, \`org/proj\`, or simply \`proj\`; with an API token, only projects the token has access to — \`list_projects\` shows them).
14821- **A lookup returns nothing when you expected results:** it may have run against a different project. The default project is stored against your user account, so it is shared with the CLI and every other session, and changing it anywhere changes it everywhere. Call \`get_project\` to see which project is in play — an empty test-run or session result already names it for you.
14822- **SSO-only organizations:** if your organization enforces a specific identity provider, a token issued outside that flow is rejected; log in again through your organization's SSO entry point.
14823- Anything else: contact us (see below).
14824
14825---
14826
14827## Support and policies
14828
14829- **Support:** [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL})
14830- **Privacy policy:** [meticulous.ai/privacy-policy](https://www.meticulous.ai/privacy-policy)
14831- **Terms of service:** [meticulous.ai/terms-conditions](https://www.meticulous.ai/terms-conditions)
14832- **Security and compliance:** [security.meticulous.ai](https://security.meticulous.ai/)
14833`},{id:"agents-skills",url:"/docs/agents/skills",document:`---
14834{
14835  "title": "Skills & Use cases"
14836}
14837---
14838
14839# {% $frontmatter.title %}
14840
14841Skills add higher-level workflows on top of the CLI and the MCP server — pre-defined workflows for reviewing test runs, running a test run end to end, debugging diffs, and more. They are Markdown files in the \`SKILL.md\` format, and the full source is released in [this repo](https://github.com/alwaysmeticulous/skills). They work the same way whichever way you connect. See [Setup](${o.AGENTS_SETUP_URL}#skills) for how to install and update them.
14842
14843- [Use cases](#use-cases)
14844  - [Ask the agent to review your test-run diffs](#ask-the-agent-to-review-your-test-run-diffs)
14845  - [Ask the agent to fix your rejected diffs](#ask-the-agent-to-fix-your-rejected-diffs)
14846  - [Ask the agent to implement a change end-to-end against Meticulous](#ask-the-agent-to-implement-a-change-end-to-end-against-meticulous)
14847  - [Ask the agent to increase coverage for your project](#ask-the-agent-to-increase-coverage-for-your-project)
14848- [Why reject and comment?](#why-reject-and-comment)
14849- [Install & update the skills](#install-update-the-skills)
14850- [Skills reference](#skills-reference)
14851
14852---
14853
14854## Use cases
14855
14856There are three core flows. They differ in who reviews and who fixes, and they stack: the agent swarms for you, the agent fixes for you, or the agent does both on its own. A fourth sits outside that loop, pointing the agent at coverage rather than at a run's diffs.
14857
14858### Ask the agent to review your test-run diffs
14859
14860> *Review the Meticulous diffs for this PR: work out which visual changes it's meant to produce from the PR description, flag and reject anything unintended, and tell me what you found.*
14861
14862The [\`meticulous-review\`](#meticulous-review) skill establishes what's expected first — from the PR description, or from the conversation if it has that context — then works through the run's representative diffs looking for the ones that don't match, using screenshots, DOM diffs and replay timelines. It reviews as a reviewer rather than as the author, and it records each unintended diff in Meticulous the way you would, rejecting real regressions and leaving a comment with its reasoning.
14863
14864If there's no run to review yet — the change isn't pushed, or isn't even committed — [\`meticulous-test\`](#meticulous-test) uploads the build and triggers the run first, then hands off to \`meticulous-review\`. To validate as you go instead of at the end, [\`meticulous-iterative-dev\`](#meticulous-iterative-dev) checks each step locally before the final cloud run.
14865
14866### Ask the agent to fix your rejected diffs
14867
14868> *I've rejected the Meticulous diffs that are real bugs, and commented on some of them. Fix the underlying code, answer on each thread, then push and check the diffs are gone on the next run.*
14869
14870The [\`meticulous-fix\`](#meticulous-fix) skill starts from the diffs you rejected and the comments you left, fixes the underlying code, replies on each thread with the outcome, and pushes — then waits for the next CI run to confirm the diffs are actually gone.
14871
14872### Ask the agent to implement a change end-to-end against Meticulous
14873
14874> *Upgrade React from v18 to v19. Nothing should change visually, so use Meticulous as the loop: trigger a run, fix whatever diffs come back, and repeat until it's clean — then open the PR.*
14875
14876The fully autonomous loop: the [\`meticulous-zero-diff-task\`](#meticulous-zero-diff-task) skill combines the two flows above into one the agent drives itself — implement, build, run, review its own diffs, fix what broke, run again — until the run comes back clean, and only then opens a PR.
14877
14878It works because the success criterion is unambiguous enough to hand over: on an upgrade, a refactor or a migration, *any* diff is a sign something broke, so there's no judgment call for the agent to get wrong and no reason for you to be in the loop until it's done. Meticulous isn't the final check here, it's what the agent iterates against.
14879
14880### Ask the agent to increase coverage for your project
14881
14882> *I want to improve the coverage for our project. Identify the code parts that no recorded session currently reaches, record sessions that exercise them, and show me which files improved.*
14883
14884The [\`meticulous-increase-coverage\`](#meticulous-increase-coverage) skill reads the project's per-file coverage and splits what it finds in two. Code a real user flow could reach but no session happens to, it traces back to the UI action that calls it, drives that action in a real browser to record a session, and includes in a new test run. Code that never executes in a browser at all, it proposes as \`.meticulousignore\` entries in a PR — so the number you're measuring stops counting code that was never coverable in the first place.
14885
14886Two caveats worth knowing about: run it on a clean \`main\`, for clean coverage information. And the skill only records sessions, but still relies on the next session selection run to pick them up, so project coverage won't improve immediately.
14887
14888---
14889
14890## Why reject and comment? {% #why-reject-and-comment %}
14891
14892Rejecting a diff and pinning a comment on it isn't just bookkeeping — it's the task list your agent works from.
14893
14894- **A rejection or comment defines the scope.** \`meticulous-fix\` works from the diffs you rejected, plus any diff you left an actionable comment on — so there's no risk of the agent "fixing" a change you meant to keep. Use **ignore** rather than reject for noise or a flake — the skill deliberately leaves those threads alone.
14895- **A comment is anchored to a point.** Comments carry coordinates on the screenshot, so the agent knows *where*, not just *which* — and it pulls the before/after images and the DOM diff for that same spot.
14896- **A comment says what "fixed" means.** "This should stay left-aligned when the label wraps" is an instruction. A bare rejection only says "this is wrong", leaving the agent to infer your intent from pixels — it will try, but one sentence of intent buys you a much better fix.
14897- **The outcome lands next to the diff.** The agent answers on your thread rather than only in its own chat window, including giving a reason if something couldn't be fixed.
14898
14899Because both sides read and write the same threads, the roles compose: let \`meticulous-review\` review a run, skim its rejections in the gallery, override or annotate the ones you see differently, then hand the set to \`meticulous-fix\`.
14900
14901---
14902
14903## Install & update the skills
14904
14905To install, and update, the skills into your project using [npx skills](https://github.com/vercel-labs/skills) (for the specified agents):
14906
14907\`\`\`bash
14908npx skills add alwaysmeticulous/skills --skill "*" --agent claude-code --agent codex --agent cursor -y
14909\`\`\`
14910
14911---
14912
14913## Skills reference
14914
14915#### meticulous-review
14916
14917Analyze a completed Meticulous test run — compare the diffs against the PR description to see what's expected, then focus on finding and flagging potential regressions. Resolves the test run from the local repo's current commit, or from an explicit test-run ID or commit SHA. Use when asked to review Meticulous test results, when babysitting a pull/merge request's Meticulous Tests CI check, or right after implementing a frontend change yourself.
14918
14919#### meticulous-fix
14920
14921Fix the visual diffs that have been reviewed and rejected on a test run, following their review comments if given. Use when a user has reviewed the results of a test run and is handing off to an agent to implement the fixes.
14922
14923#### meticulous-test
14924
14925Run a Meticulous test run after implementing a frontend change, then hand off to the \`meticulous-review\` skill to classify each visual change as intended or unintended. Uploads the build once — the same build can be re-triggered against different bases without rebuilding — and it works with uncommitted changes. Use when implementing a feature autonomously end-to-e
14925nd before creating a PR.
14926
14927#### meticulous-zero-diff-task
14928
14929Implement a task for which no visual diffs are expected end to end, using Meticulous to drive the implementation to a clean visual diff before opening a PR. Use when the task's whole premise is that the UI shouldn't change — a dependency/version upgrade, a code refactor, a migration, or similar.
14930
14931#### meticulous-increase-coverage
14932
14933Increase coverage for a project by tracing specific under-covered files back to a real UI action in the codebase, driving that action with a real recorded browser session, and validating the improvement with a clean coverage comparison. Also opens a PR proposing \`.meticulousignore\` entries for code that structurally never executes in-browser. Use when asked to increase coverage, to find untested code, or to add \`.meticulousignore\` entries for a project.
14934
14935#### meticulous-iterative-dev
14936
14937Iterative frontend development loop using Meticulous for per-step visual validation. Use when implementing a multi-step frontend change and want to catch visual regressions and unintended side effects at each step, before the final cloud test run.
14938
14939#### meticulous-simulate-and-diff
14940
14941Run a Meticulous session simulation against a live URL and analyze the visual output — either by inspecting screenshots directly (quick-check mode) or by comparing pixel and HTML diffs against a base replay. Use when checking whether a code change has introduced visual regressions for a specific session.
14942
14943#### meticulous-use-session-data
14944
14945Download and use structured Meticulous session data (user flows + network mocks) for testing code changes locally. Use when you need to understand what user interactions and API calls a test c
14945overs, or when you want network mocks for writing tests.
14946
14947---
14948
14949### Supporting skills
14950
14951#### meticulous-cli
14952
14953Overview of the Meticulous CLI tool and its global options. Use when asking about the meticulous CLI in general, available commands, or global flags that apply to all commands.
14954
14955#### meticulous-cli-update
14956
14957Checks whether the Meticulous CLI (\`@alwaysmeticulous/cli\`) and the skills themselves are installed and up to date, and installs/updates them if not. Invoked at the start of every other Meticulous skill, since both are under active development with frequent changes and improvements.
14958`},{id:"cli-commands",url:"/docs/reference/cli-commands",document:`---
14959{
14960  "title": "CLI Commands Reference"
14961}
14962---
14963
14964# {% $frontmatter.title %}
14965
14966Complete reference for Meticulous CLI commands, flags, and usage patterns.
14967
14968---
14969
14970## Installation
14971
14972Install the CLI globally or use npx:
14973
14974\`\`\`bash
14975# Using npx (recommended)
14976npx @alwaysmeticulous/cli [command]
14977
14978# Or install globally
14979npm install -g @alwaysmeticulous/cli
14980meticulous [command]
14981\`\`\`
14982
14983---
14984
14985## Commands Overview
14986
14987| Command | Purpose | Use Case |
14988|---------|---------|----------|
14989| \`onboard\` | Install Meticulous using local Claude Code or Codex | Initial recorder and CI setup |
14990| \`ci run-with-tunnel\` | Run tests in cloud via tunnel | CI testing with local app |
14991| \`ci upload-assets\` | Upload and test static assets | CI testing for static sites |
14992| \`ci upload-asset-chunk\` | Upload one named, versioned asset chunk | Multi-bundle deployments |
14993| \`ci run-with-uploaded-asset-chunks\` | Trigger a test run against uploaded chunks | Multi-bundle deployments |
14994| \`ci upload-container\` | Upload Docker container and test | CI testing with containers |
14995| \`ci agent-test\` | Upload a build and launch an agent to explore the PR | Beta, opt-in Agent swarm |
14996| \`ci run-local\` | Run all replay test cases locally | Local test execution |
14997| \`ci prepare\` | Ensure base run exists | CI setup |
14998| \`ci label-commit\` | Attach labels to a commit | Marking commits as not relevant for testing |
14999| \`ci start-tunnel\` | Start secure tunnel | Manual testing/debugging |
15000| \`simulate\` (alias: \`replay\`) | Replay session locally | Local debugging |
15001| \`record session\` | Record a user session | Session recording |
15002| \`record login\` | Record a login flow | Login flow recording |
15003| \`crawl\` | Crawl your app to record sessions and create a test run | Bootstrapping session coverage |
15004| \`auth login\` | Force a fresh browser login and select a project | Authentication |
15005| \`auth whoami\` | Show current user | Authentication check |
15006| \`auth logout\` | Revoke and clear stored tokens | Authentication |
15007| \`auth get-project\` | Print your default project | Authentication |
15008| \`auth set-project\` | Choose your default project | Authentication |
15009| \`auth list-projects\` | List the projects you can access | Authentication |
15010| \`project show\` | Show linked project | Project info |
15011| \`project upload-source\` | Upload a source-code archive for a given commit | Source coverage / CI |
15012| \`download session\` | Download a recorded session | Debugging |
15013| \`download replay\` | Download a replay | Debugging |
15014| \`download test-run\` | Download a test run | Debugging |
15015| \`local relevant-sessions\` | Find sessions covering the current branch's code changes | Local development |
15016| \`debug replay\` | Set up a debug workspace for a single replay | Investigating a replay |
15017| \`debug replay-diff\` | Set up a debug workspace for a specific replay diff | Investigating a diff |
15018| \`debug clean\` | Clean up debug workspaces | Debug workspace maintenance |
15019| \`agent upload-build\` | Upload a build (static assets or container) and capture a deployment ID | Agent/programmatic use |
15020| \`agent trigger-test-run\` | Trigger a test run against an uploaded build | Agent/programmatic use |
15021| \`agent test-run-diffs\` | List replay diffs for a test run with summary | Agent/programmatic use |
15022| \`agent diff-comments\` | Get review comments for a replay-diff screenshot | Agent/programmatic use |
15023| \`agent approve-diff\` | Agent-approve a screenshot diff, optionally commenting why, if the project allows it | Agent/programmatic use |
15024| \`agent reject-diff\` | Agent-reject a screenshot diff and comment why | Agent/programmatic use |
15025| \`agent ignore-diff\` | Agent-ignore a screenshot diff as unrelated to the change and comment why; a real ignore only if the project allows it | Agent/programmatic use |
15026| \`agent create-diff-comment\` | Start a review comment thread on a screenshot diff | Agent/programmatic use |
15027| \`agent reply-to-diff-comment\` | Reply to a review comment thread | Agent/programmatic use |
15028| \`agent dom-diff\` | Get the DOM diff for a replay-diff screenshot | Agent/programmatic use |
15029| \`agent image-urls\` | Get screenshot image URLs for a replay-diff screenshot | Agent/programmatic use |
15030| \`agent image-files\` | Download screenshot images to \`~/.meticulous/agent-images\` | Agent/programmatic use |
15031| \`agent timeline-diff\` | Get the timeline diff for a replay diff | Agent/programmatic use |
15032| \`agent test-run-check\` | Get a builtin or custom non-visual check report for a test run, or list available check IDs with \`--availableIds\` | Agent/programmatic use |
15033| \`agent check-comments\` | Get the reasons recorded with agent decisions on a non-visual check of a test run | Agent/programmatic use |
15034| \`agent approve-check\` | Agent-approve a failing non-visual check, optionally with a reason, if the project allows it | Agent/programmatic use |
15035| \`agent reject-check\` | Agent-reject a failing non-visual check, with a reason | Agent/programmatic use |
15036| \`agent ignore-check\` | Agent-ignore a failing non-visual check as unrelated to the change, with a reason, if the project allows it | Agent/programmatic use |
15037| \`agent test-run-for-commit\` | Look up the latest test run for a commit (defaults to git HEAD) | Agent/programmatic use |
15038| \`agent test-runs\` | List a project's pull request test runs, or its base test runs, newest first | Agent/programmatic use |
15039| \`agent sessions\` | List a project's recorded sessions, newest first by default, optionally narrowed to the selected set | Agent/programmatic use |
15040| \`agent test-run-stats\` | Export reporting statistics for a project's test runs | Agent/programmatic use |
15041| \`agent project-daily-stats\` | Export daily project reporting statistics | Agent/programmatic use |
15042| \`agent test-run-event-stats\` | Export a project's test-run reporting events | Agent/programmatic use |
15043| \`agent js-coverage\` | Get JS coverage for a replay or a whole test run, per file or as aggregate totals | Agent/programmatic use |
15044| \`agent js-coverage-diff\` | Get the JS coverage diff (base vs head) for a replay diff, or between two whole test runs | Agent/programmatic use |
15045| \`agent upload-build\` | Upload a build (static assets or container) and capture a deployment ID | Agent/programmatic use |
15046| \`agent trigger-test-run\` | Trigger a test run against an uploaded build | Agent/programmatic use |
15047| \`agent complete-base-run\` | Replay the selected sessions a base run has not run yet | Agent/programmatic use |
15048| \`agent promote-sessions\` | Add sessions a pinned-session test run replayed to the selected set | Agent/programmatic use |
15049| \`agent submit-feedback\` | Submit free-form feedback about Meticulous to the Meticulous team | Agent/programmatic use |
15050| \`schema\` | Print the CLI command schema as JSON | Agent/programmatic use |
15051
15052For a closer look at the \`agent\` and \`auth\` commands — including their flags and how they compose into agent workflows — see [CLI commands for agents](${o.AGENTS_CLI_COMMANDS_URL}).
15053
15054---
15055
15056## onboard
15057
15058Install Meticulous in the current Git repository using your local Claude Code
15059or Codex. The command reviews the frontend application and prepares a pull
15060request with recorder and CI configuration. Model inference runs through your
15061own coding-agent account, not Meticulous-hosted inference.
15062
15063### Authentication
15064
15065If you are not already logged in, \`onboard\` opens a browser to sign in and
15066then selects the Meticulous project. On a remote or sandboxed machine, run
15067\`npx @alwaysmeticulous/cli auth login --device\` first. Alternatively, pass
15068\`--apiToken\`.
15069
15070### Examples
15071
15072\`\`\`bash
15073# Interactive setup from the connected application repository
15074npx @alwaysmeticulous/cli onboard --project="<ORGANIZATION>/<PROJECT>"
15075
15076# Choose an app in a monorepo and use Claude Code
15077npx @alwaysmeticulous/cli onboard \\
15078  --project="<ORGANIZATION>/<PROJECT>" \\
15079  --app="apps/web" \\
15080  --agent=claude
15081
15082# Prepare the workspace without launching an agent
15083npx @alwaysmeticulous/cli onboard --printOnly
15084\`\`\`
15085
15086Key options include \`--cwd\`, \`--project\`, \`--app\`, \`--agent\`,
15087\`--model\`, \`--headless\`, \`--auto\`, \`--printOnly\`, and
15088\`--apiToken\`.
15089
15090---
15091
15092## ci run-with-tunnel
15093
15094Run Meticulous tests in the cloud against a locally-running application.
15095
15096### Synopsis
15097
15098\`\`\`bash
15099npx @alwaysmeticulous/cli ci run-with-tunnel \\
15100  --apiToken="<token>" \\
15101  --appUrl="<url>" \\
15102  [options]
15103\`\`\`
15104
15105### Required Flags
15106
15107#### \`--apiToken\`
15108
15109**Type**: String
15110**Description**: Your Meticulous API token
15111**How to get**: From Meticulous dashboard project settings
15112
15113**Example**:
15114\`\`\`bash
15115--apiToken="met_live_abc123..."
15116\`\`\`
15117
15118**Note**: Can also be set via \`METICULOUS_API_TOKEN\` environment variable.
15119
15120---
15121
15122#### \`--appUrl\`
15123
15124**Type**: String
15125**Description**: URL where your app is running
15126**Format**: Full URL including protocol and port
15127
15128**Examples**:
15129\`\`\`bash
15130--appUrl="http://localhost:3000"
15131--appUrl="http://localhost:8080"
15132--appUrl="https://localhost:3000"
15133\`\`\`
15134
15135---
15136
15137### Optional Flags
15138
15139#### \`--commitSha\`
15140
15141**Type**: String
15142**Description**: Commit SHA being tested
15143**Default**: Auto-detected from git
15144
15145**Example**:
15146\`\`\`bash
15147--commitSha="$GITHUB_SHA"
15148--commitSha="abc123def456..."
15149\`\`\`
15150
15151---
15152
15153#### \`--companionAssetsFolder\`
15154
15155**Type**: String (path)
15156**Description**: Path to local folder with static assets to upload
15157**Default**: None
15158**Requires**: Must also provide \`--companionAssetsRegex\`
15159
15160**Example**:
15161\`\`\`bash
15162--companionAssetsFolder="companion-assets"
15163\`\`\`
15164
15165---
15166
15167#### \`--companionAssetsRegex\`
15168
15169**Type**: String (regex)
15170**Description**: Regex pattern for requests to serve from companion assets
15171**Default**: None
15172**Requires**: Must also provide \`--companionAssetsFolder\`
15173
15174**Example**:
15175\`\`\`bash
15176--companionAssetsRegex="^/_next/static/"
15177\`\`\`
15178
15179---
15180
15181#### \`--proxyAllUrls\`
15182
15183**Type**: Boolean
15184**Description**: Proxy all URLs through tunnel (not just app URL)
15185**Default**: false
15186
15187**Example**:
15188\`\`\`bash
15189--proxyAllUrls
15190\`\`\`
15191
15192**Use case**: Multi-server applications (frontend + API on different ports)
15193
15194---
15195
15196#### \`--rewriteHostnameToAppUrl\`
15197
15198**Type**: Boolean
15199**Description**: Rewrite request hostname to match app U
15199RL
15200**Default**: false
15201
15202**Example**:
15203\`\`\`bash
15204--rewriteHostnameToAppUrl
15205\`\`\`
15206
15207**Use case**: When HTML contains absolute URLs
15208
15209---
15210
15211#### \`--secureTunnelHost\`
15212
15213**Type**: String
15214**Description**: Custom tunnel server host
15215**Default**: Meticulous production tunnel
15216**Note**: For Meticulous team use only
15217
15218---
15219
15220#### \`--hadPreparedForTests\`
15221
15222**Type**: Boolean
15223**Description**: Indicate that \`meticulous ci prepare\` was already run
15224**Default**: false
15225
15226---
15227
15228### Complete Example
15229
15230\`\`\`bash
15231# Basic usage
15232npx @alwaysmeticulous/cli ci run-with-tunnel \\
15233  --apiToken="$METICULOUS_API_TOKEN" \\
15234  --appUrl="http://localhost:3000"
15235
15236# With companion assets (Next.js)
15237npx @alwaysmeticulous/cli ci run-with-tunnel \\
15238  --apiToken="$METICULOUS_API_TOKEN" \\
15239  --appUrl="http://localhost:3000" \\
15240  --companionAssetsFolder="companion-assets" \\
15241  --companionAssetsRegex="^/_next/static/"
15242
15243# Multi-server app
15244npx @alwaysmeticulous/cli ci run-with-tunnel \\
15245  --apiToken="$METICULOUS_API_TOKEN" \\
15246  --appUrl="http://localhost:3000" \\
15247  --proxyAllUrls
15248\`\`\`
15249
15250---
15251
15252### Exit Codes
15253
15254| Code | Meaning |
15255|------|---------|
15256| 0 | Success - all tests passed or diffs approved |
15257| 1 | Failure - tests failed or unapproved diffs |
15258| 2 | Error - configuration or connection error |
15259
15260---
15261
15262## ci agent-test
15263
15264{% callout type="info" title="Beta opt-in" %}
15265Agent swarm is currently in beta and is not available to every customer. Your Meticulous project must be explicitly enabled before this command can launch. [Set up Agent swarm](${o.AGENT_SWARM_DOCS_URL}) explains how to request access and configure the workflow.
15266{% /callout %}
15267
15268Upload one build target and launch a Meticulous-hosted agent that explores the pull request and creates additional recorded sessions.
15269
15270### Synopsis
15271
15272\`\`\`bash
15273npx @alwaysmeticulous/cli ci agent-test \\
15274  --assetsDir="<path-to-built-assets>" \\
15275  --commitSha="<pr-head-sha>" \\
15276  [options]
15277\`\`\`
15278
15279Provide exactly one target:
15280
15281- \`--assetsDir\` — a built static frontend directory.
15282- \`--localImageTag\` — a locally built Docker image.
15283
15284By default, the agent reads routes and flows to exercise from \`.meticulous/agent-swarm-instructions.md\` in the repository at the commit being tested. Use \`--instructionsFile\` to override those instructions for one CLI-launched run. With an uploaded frontend, \`--backendUrl\` proxies configured relative paths (\`--backendProxyPaths\`, default \`/api\`) to a public HTTPS staging backend. It cannot be combined with \`--enableLocalMocks\`. For absolute cross-origin requests, contact Meticulous to add the required egress policy for your project.
15285
15286For a pull request workflow, pass \`github.event.pull_request.head.sha || github.sha\` as \`--commitSha\`, rather than only \`github.sha\`. Use \`--dryRun\` to validate options without launching an agent.
15287
15288---
15289
15290## ci upload-assets
15291
15292Upload static assets and run tests in the cloud.
15293
15294### Synopsis
15295
15296\`\`\`bash
15297npx @alwaysmeticulous/cli ci upload-assets \\
15298  --apiToken="<token>" \\
15299  --appDirectory="<path>" \\
15300  [options]
15301\`\`\`
15302
15303### Required Flags
15304
15305#### \`--apiToken\`
15306
15307**Type**: String
15308**Description**: Your Meticulous API token
15309
15310---
15311
15312#### \`--appDirectory\`
15313
15314**Type**: String (path)
15315**Description**: Path to directory containing built static assets
15316**Common values**: \`dist\`, \`build\`, \`out\`
15317
15318**Examples**:
15319\`\`\`bash
15320--appDirectory="dist"        # Vite
15321--appDirectory="build"       # Create React App
15322--appDirectory="out"         # Next.js static export
15323\`\`\`
15324
15325---
15326
15327### Optional Flags
15328
15329#### \`--commitSha\`
15330
15331**Type**: String
15332**Description**: Commit SHA being tested
15333**Default**: Auto-detected from git
15334
15335---
15336
15337#### \`--rewrites\`
15338
15339**Type**: String (JSON)
15340**Description**: URL rewrite rules in Vercel format
15341**Use case**: SPA routing, redirects
15342
15343**Example**:
15344\`\`\`bash
15345--rewrites='[{"source":"/(.*)", "destination":"/index.html"}]'
15346\`\`\`
15347
15348**Common patterns**:
15349
15350**SPA routing**:
15351\`\`\`json
15352[{"source": "/(.*)", "destination": "/index.html"}]
15353\`\`\`
15354
15355**API proxy**:
15356\`\`\`json
15357[{"source": "/api/(.*)", "destination": "https://api.example.com/$1"}]
15358\`\`\`
15359
15360---
15361
15362#### \`--waitForBase\`
15363
15364**Type**: Boolean
15365**Description**: Wait for base test run
15366**Default**: false
15367
15368---
15369
15370#### \`--waitForTestRunToComplete\`
15371
15372**Type**: Boolean
15373**Description**: After the upload succeeds and Meticulous has started a test run, keep polling until that run reaches a terminal status, then exit non-zero if the run failed.
15374
15375**Default**: false (omit the flag)
15376
15377**Standard CI setups should leave this flag off.** For GitHub, GitLab, Buildkite, CircleCI, and similar pipelines, the usual pattern is: build, run \`ci upload-assets\` (or \`ci upload-container\`) to upload and trigger a run, then let the job exit. Meticulous reports progress and outcomes on the pull request or through your VCS integration. Holding the whole CI job open until every replay finishes makes pipelines slower, rarely adds value, and tooling (including some AI code reviewers) often suggests this flag because the name sounds helpful.
15378
15379**Why avoid it by default:** the run can still have background work in some configurations (for example lazy session execution), so a naive "wait until complete" loop may exit too early or fail with errors that are hard to interpret if you were only trying to "wait for Meticulous."
15380
15381**When it may be appropriate:** automation that deliberately must block until the run is fully finished (for example internal regression checks where the process relies on the CLI exit code). If you are onboarding a new project or wiring PR checks, you almost never need this flag.
15382
15383---
15384
15385#### \`--json\`
15386
15387**Type**: Boolean
15388**Description**: Print one machine-readable JSON result on stdout. See [Machine-readable output](#machine-readable-output).
15389**Default**: \`false\`
15390
15391---
15392
15393### Complete Example
15394
15395\`\`\`bash
15396# Basic usage
15397npx @alwaysmeticulous/cli ci upload-assets \\
15398  --apiToken="$METICULOUS_API_TOKEN" \\
15399  --appDirectory="dist"
15400
15401# With SPA routing
15402npx @alwaysmeticulous/cli ci upload-assets \\
15403  --apiToken="$METICULOUS_API_TOKEN" \\
15404  --appDirectory="dist" \\
15405  --rewrites='[{"source":"/(.*)", "destination":"/index.html"}]'
15406
15407# With commit SHA
15408npx @alwaysmeticulous/cli ci upload-assets \\
15409  --apiToken="$METICULOUS_API_TOKEN" \\
15410  --appDirectory="build" \\
15411  --commitSha="$CI_COMMIT_SHA"
15412\`\`\`
15413
15414---
15415
15416### Exit Codes
15417
15418| Code | Meaning |
15419|------|---------|
15420| 0 | Success |
15421| 1 | Failure |
15422| 2 | Error |
15423
15424---
15425
15426## ci upload-asset-chunk
15427
15428Upload a named, versioned chunk of static assets to Meticulous for incremental deployments.
15429
15430### Synopsis
15431
15432\`\`\`bash
15433npx @alwaysmeticulous/cli ci upload-asset-chunk \\
15434  --apiToken="<token>" \\
15435  --chunkName="<name>" \\
15436  --chunkVersionId="<version>" \\
15437  --chunkAssetsDirectory="<path>" \\
15438  [options]
15439\`\`\`
15440
15441### Required Flags
15442
15443#### \`--apiToken\`
15444
15445**Type**: String
15446**Description**: Your Meticulous API token
15447
15448**Note**: Can also be set via \`METICULOUS_API_TOKEN\` environment variable.
15449
15450---
15451
15452#### \`--chunkName\`
15453
15454**Type**: String
15455**Description**: Logical name of the asset chunk (e.g. \`app\`, \`vendor\`).
15456
15457**Example**:
15458\`\`\`bash
15459--chunkName="app"
15460\`\`\`
15461
15462---
15463
15464#### \`--chunkVersionId\`
15465
15466**Type**: String
15467**Description**: Version identifier for this chunk (e.g. content hash or build id). Chunks are deduped by (chunkName, chunkVersionId).
15468
15469**Example**:
15470\`\`\`bash
15471--chunkVersionId="$CI_COMMIT_SHA"
15472\`\`\`
15473
15474---
15475
15476#### \`--chunkAssetsDirectory\`
15477
15478**Type**: String (path)
15479**Description**: Directory whose contents should be packaged into this chunk.
15480
15481**Example**:
15482\`\`\`bash
15483--chunkAssetsDirectory="dist"
15484\`\`\`
15485
15486---
15487
15488### Optional Flags
15489
15490#### \`--chunkAssetsDirectoryPrefix\`
15491
15492**Type**: String
15493**Description**: Path prefix prepended to every entry in the chunk (e.g. \`static/assets\`). Files in \`chunkAssetsDirectory\` will be served under this prefix at replay time.
15494
15495**Example**:
15496\`\`\`bash
15497--chunkAssetsDirectoryPrefix="static/assets"
15498\`\`\`
15499
15500---
15501
15502#### \`--commitSha\`
15503
15504**Type**: String
15505**Description**: Commit SHA being tested
15506**Default**: Auto-detected from git
15507
15508---
15509
15510#### \`--force\`
15511
15512**Type**: Boolean
15513**Description**: Re-upload even if a chunk with the same \`--chunkName\` and \`--chunkVersionId\` is already marked as uploaded on the server. Use only for recovery (e.g., a corrupted S3 object). The server will over
15513write the existing chunk; downstream test runs that already referenced the old bytes will resolve to the new ones.
15514**Default**: \`false\`
15515
15516**Example**:
15517\`\`\`bash
15518--force
15519\`\`\`
15520
15521---
15522
15523### Complete Example
15524
15525\`\`\`bash
15526npx @alwaysmeticulous/cli ci upload-asset-chunk \\
15527  --apiToken="$METICULOUS_API_TOKEN" \\
15528  --chunkName="app" \\
15529  --chunkVersionId="$CI_COMMIT_SHA" \\
15530  --chunkAssetsDirectory="dist"
15531\`\`\`
15532
15533---
15534
15535### Exit Codes
15536
15537| Code | Meaning |
15538|------|---------|
15539| 0 | Success |
15540| 1 | Failure |
15541| 2 | Error |
15542
15543---
15544
15545## ci run-with-uploaded-asset-chunks
15546
15547Trigger a test run against already-uploaded asset chunks. Pair with \`ci upload-asset-chunk\`.
15548
15549### Synopsis
15550
15551\`\`\`bash
15552npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
15553  --apiToken="<token>" \\
15554  --commitSha="<sha>" \\
15555  --assetReferencesManifest="<path>" \\
15556  [options]
15557\`\`\`
15558
15559### Required Flags
15560
15561#### \`--apiToken\`
15562
15563**Type**: String
15564**Description**: Your Meticulous API token
15565
15566---
15567
15568#### \`--assetReferencesManifest\`
15569
15570**Type**: String (path)
15571**Description**: Path to a JSON file containing a list of references to previously uploaded asset chunks (see \`ci upload-asset-chunk\`). Each entry is either \`{ name, versionId }\` (an explicit chunk version) or \`{ name, versionLookup: "latest-in-history" }\` (resolves the version of an unchanged chunk from the base test run's history; the base is inferred automatically for GitHub projects, or pass \`--baseSha\` to override it). Chunk names must be unique. Chunked analog of \`--appDirectory\` / \`--appZip\` on \`ci upload-assets\`.
15572
15573**File format**:
15574\`\`\`json
15575[
15576  { "name": "app", "versionId": "ad8a8da9aaaweaad9" },
15577  { "name": "plugin-1", "versionLookup": "latest-in-history" }
15578]
15579\`\`\`
15580
15581**Example**:
15582\`\`\`bash
15583--assetReferencesManifest="./manifest.json"
15584\`\`\`
15585
15586---
15587
15588### Optional Flags
15589
15590#### \`--commitSha\`
15591
15592**Type**: String
15593**Description**: Commit SHA being tested
15594**Default**: Auto-detected from git
15595
15596---
15597
15598#### \`--baseSha\`
15599
15600**Type**: String
15601**Description**: The base commit SHA to compare against. Intended for custom test run triggers. Cannot be combined with \`--repoDirectory\`.
15602
15603---
15604
15605#### \`--gitDiffOutput\`
15606
15607**Type**: String
15608**Description**: Raw git diff output between the base and head commits. Requires \`--baseSha\`. Cannot be combined with \`--repoDirectory\`.
15609
15610---
15611
15612#### \`--repoDirectory\`
15613
15614**Type**: String (path)
15615**Description**: The path to a git repository. Intended for custom test run triggers. Automatically infers \`--commitSha\`, \`--baseSha\`, and \`--gitDiffOutput\` from the repo. Cannot be combined with \`--commitSha\`, \`--baseSha\`, or \`--gitDiffOutput\`.
15616
15617---
15618
15619#### \`--rewrites\`
15620
15621**Type**: String (JSON)
15622**Description**: URL rewrite rules in Vercel \`serve-handler\` format.
15623**Default**: \`'[]'\` (falls back to \`{ source: "**", destination: "/index.html" }\`)
15624
15625**Example**:
15626\`\`\`bash
15627--rewrites='[{"source":"/(.*)", "destination":"/index.html"}]'
15628\`\`\`
15629
15630---
15631
15632#### \`--sessionFilter\`
15633
15634**Type**: String (path)
15635**Description**: Path to a JSON file restricting which sessions the test run replays. A session is replayed if its start URL matches at least one of the regexes ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)). If omitted, all selected sessions are replayed. See [Filter Sessions by Start URL](/docs/how-to/filter-sessions-by-start-url).
15636
15637**File format**:
15638\`\`\`json
15639{
15640  "session-start-url-matches-any-regex": ["/checkout/", "/settings/"]
15641}
15642\`\`\`
15643
15644**Example**:
15645\`\`\`bash
15646--sessionFilter="./session-filter.json"
15647\`\`\`
15648
15649If the filter excludes every session, no test run is triggered and the command exits with code \`4\` rather than the
15650generic \`1\`, so a pipeline can tell "nothing to test" apart from a real failure. An empty regex list (or one containing
15651only blank strings) also exits with \`4\`, but still creates the deployment, so later runs whose base is this commit —
15652such as a stacked pull request — can compare against it.
15653
15654---
15655
15656#### \`--waitForBase\`
15657
15658**Type**: Boolean
15659**Description**: If true, wait for a base test run to be created before triggering a test run.
15660**Default**: \`true\`
15661
15662---
15663
15664#### \`--waitForTestRunToComplete\`
15665
15666**Type**: Boolean
15667**Description**: Block until the triggered test run finishes. Only for runs tied to a local branch: requires \`--repoDirectory\`, or both \`--baseSha\` and \`--gitDiffOutput\`. Implies \`--waitForBase\`.
15668**Default**: \`false\`
15669
15670---
15671
15672#### \`--dryRun\`
15673
15674**Type**: Boolean
15675**Description**: Print what would be triggered without making the API call.
15676**Default**: \`false\`
15677
15678---
15679
15680#### \`--json\`
15681
15682**Type**: Boolean
15683**Description**: Print one machine-readable JSON result on stdout. See [Machine-readable output](#machine-readable-output).
15684**Default**: \`false\`
15685
15686---
15687
15688### Complete Example
15689
15690\`\`\`bash
15691# manifest.json
15692# [
15693#   { "name": "app", "versionId": "ad8a8da9aaaweaad9" },
15694#   { "name": "plugin-1", "versionId": "dd8ffdaa9dfedebb3" }
15695# ]
15696
15697npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
15698  --apiToken="$METICULOUS_API_TOKEN" \\
15699  --commitSha="$CI_COMMIT_SHA" \\
15700  --assetReferencesManifest="./manifest.json"
15701\`\`\`
15702
15703---
15704
15705### Exit Codes
15706
15707| Code | Meaning |
15708|------|---------|
15709| 0 | Success |
15710| 1 | Failure |
15711| 4 | No test run triggered: \`--sessionFilter\` excluded every session that would otherwise have been replayed |
15712
15713---
15714
15715## simulate
15716
15717Replay a session locally for debugging.
15718
15719### Synopsis
15720
15721\`\`\`bash
15722npx @alwaysmeticulous/cli simulate \\
15723  --sessionId="<id>" \\
15724  --appUrl="<url>" \\
15725  [options]
15726\`\`\`
15727
15728### Required Flags
15729
15730#### \`--sessionId\`
15731
15732**Type**: String
15733**Description**: ID of session to replay
15734**How to get**: From Meticulous dashboard or test run
15735
15736**Example**:
15737\`\`\`bash
15738--sessionId="ses_abc123..."
15739\`\`\`
15740
15741---
15742
15743#### \`--appUrl\`
15744
15745**Type**: String
15746**Description**: URL where your app is running locally
15747
15748**Example**:
15749\`\`\`bash
15750--appUrl="http://localhost:3000"
15751\`\`\`
15752
15753---
15754
15755### Optional Flags
15756
15757#### \`--apiToken\`
15758
15759**Type**: String
15760**Description**: Your Meticulous API token
15761**Note**: Required if session is private
15762
15763---
15764
15765#### \`--headless\`
15766
15767**Type**: Boolean
15768**Description**: Run browser in headless mode
15769**Default**: false
15770
15771**Example**:
15772\`\`\`bash
15773--headless
15774\`\`\`
15775
15776---
15777
15778#### \`--devtools\`
15779
15780**Type**: Boolean
15781**Description**: Open browser DevTools automatically
15782**Default**: false
15783
15784**Example**:
15785\`\`\`bash
15786--devtools
15787\`\`\`
15788
15789---
15790
15791### Complete Example
15792
15793\`\`\`bash
15794# Basic replay
15795npx @alwaysmeticulous/cli simulate \\
15796  --sessionId="ses_abc123..." \\
15797  --appUrl="http://localhost:3000"
15798
15799# With DevTools for debugging
15800npx @alwaysmeticulous/cli simulate \\
15801  --sessionId="ses_abc123..." \\
15802  --appUrl="http://localhost:3000" \\
15803  --devtools
15804
15805# Headless mode for CI
15806npx @alwaysmeticulous/cli simulate \\
15807  --sessionId="ses_abc123..." \\
15808  --appUrl="http://localhost:3000" \\
15809  --headless
15810\`\`\`
15811
15812---
15813
15814### Exit Codes
15815
15816| Code | Meaning |
15817|------|---------|
15818| 0 | Replay completed successfully |
15819| 1 | Replay failed |
15820
15821---
15822
15823## crawl
15824
15825Crawl your app from one or more start URLs to record sessions and create a test run from them. Opens a local headed browser at the first start URL and pauses so you can manually log in before crawling starts — useful for bootstrapping session coverage on apps that require a login.
15826
15827Passing several start URLs crawls each of them in turn in the same browser, so a single login covers the whole list, and each URL is opened by a full page load so that it records a session of its own.
15828
15829{% callout type="warning" %}
15830Recording starts as soon as the browser opens, so the login flow (including any credentials you type) is recorded as part of the first session.
15831{% /callout %}
15832
15833### Synopsis
15834
15835\`\`\`bash
15836npx @alwaysmeticulous/cli crawl \\
15837  --apiToken="<token>" \\
15838  --startUrl="<url>" \\
15839  [options]
15840\`\`\`
15841
15842### Required Flags
15843
15844#### \`--startUrl\`
15845
15846**Type**: String (repeatable)
15847**Description**: A URL to crawl. Repeat the flag, or give it several values, to crawl a list of URLs in order — they share one browser (and so one login), and each is loaded afresh so that it records its own session
15848
15849**Example**:
15850\`\`\`bash
15851--startUrl="https://app.example.com"
15852
15853# Several URLs behind one login
15854--startUrl="https://app.example.com/dashboard" \\
15855  --startUrl="https://app.example.com/settings" \\
15856  --startUrl="https://app.example.com/billing"
15857\`\`\`
15858
15859---
15860
15861### Optional Flags
15862
15863#### \`--apiToken\`
15864
15865**Type**: String
15866**Description**: The API token of the project to record sessions into
15867**Note**: When omitted, the command uses your OAuth login (run \`meticulous auth login\` and \`meticulous auth set-project\` to choose the project), falling back to the \`METICULOUS_API_TOKEN\` environment variable or your locally stored token
15868
15869---
15870
15871#### \`--crawlingTimeoutSeconds\`
15872
15873**Type**: Number
15874**Description**: The maximum total time in seconds to spend crawling, shared out between the start URLs — each remaining URL gets an equal share of the time left, so one that finishes early hands its unused budget to the ones after it (time spent logging in doesn't count)
15875**Default**: 120
15876
15877---
15878
15879#### \`--maxNumSessions\`
15880
15881**Type**: Number
15882**Description**: The maximum number of sessions to record
15883**Default**: 200
15884
15885---
15886
15887#### \`--skipTestRun\`
15888
15889**Type**: Boolean
15890**Description**: Don't create a test run from the recorded sessions
15891**Default**: false
15892
15893---
15894
15895### Complete Example
15896
15897\`\`\`bash
15898# Crawl for 2 minutes and create a test run from the recorded sessions
15899npx @alwaysmeticulous/cli crawl \\
15900  --apiToken="<token>" \\
15901  --startUrl="https://app.example.com"
15902
15903# Longer crawl, sessions only (no test run)
15904npx @alwaysmeticulous/cli crawl \\
15905  --apiToken="<token>" \\
15906  --startUrl="https://app.example.com" \\
15907  --crawlingTimeoutSeconds=600 \\
15908  --skipTestRun
15909
15910# A list of URLs behind one login, 10 minutes shared between them
15911npx @alwaysmeticulous/cli crawl \\
15912  --apiToken="<token>" \\
15913  --startUrl="https://app.example.com/dashboard" \\
15914  --startUrl="https://app.example.com/settings" \\
15915  --startUrl="https://app.example.com/billing" \\
15916  --crawlingTimeoutSeconds=600
15917\`\`\`
15918
15919When the browser opens, log in if your app requires it, then press Enter in the terminal to start crawling. Once the crawl finishes the CLI prints the URL of the created test run.
15920
15921---
15922
15923### Exit Codes
15924
15925| Code | Meaning |
15926|------|---------|
15927| 0 | Crawl completed successfully |
15928| 1 | Crawl failed or no sessions were recorded |
15929
15930---
15931
15932## ci start-tunnel
15933
15934Start a secure tunnel for manual testing and debugging.
15935
15936### Synopsis
15937
15938\`\`\`bash
15939npx @alwaysmeticulous/cli ci start-tunnel \\
15940  --port=<port> \\
15941  [options]
15942\`\`\`
15943
15944### Required Flags
15945
15946#### \`--port\` / \`-p\`
15947
15948**Type**: Number
15949**Description**: Port your local server is running on
15950
15951**Example**:
15952\`\`\`bash
15953--port=3000
15954-p 3000
15955\`\`\`
15956
15957---
15958
15959### Optional Flags
15960
15961#### \`--apiToken\`
15962
15963**Type**: String
15964**Description**: Your Meticulous API token
15965**Note**: Required for authentication
15966
15967---
15968
15969#### \`--localHost\` / \`-l\`
15970
15971**Type**: String
15972**Description**: Host to tunnel to
15973**Default**: localhost
15974
15975**Example**:
15976\`\`\`bash
15977--localHost=127.0.0.1
15978\`\`\`
15979
15980---
15981
15982#### \`--localHttps\`
15983
15984**Type**: Boolean
15985**Description**: Connect to local HTTPS server
15986**Default**: false
15987
15988**Example**:
15989\`\`\`bash
15990--localHttps
15991\`\`\`
15992
15993---
15994
15995#### \`--localCert\`
15996
15997**Type**: String (path)
15998**Description**: Path to SSL certificate file
15999
16000**Example**:
16001\`\`\`bash
16002--localCert="./certs/server.crt"
16003\`\`\`
16004
16005---
16006
16007#### \`--localKey\`
16008
16009**Type**: String (path)
16010**Description**: Path to SSL key file
16011
16012**Example**:
16013\`\`\`bash
16014--localKey="./certs/server.key"
16015\`\`\`
16016
16017---
16018
16019#### \`--localCa\`
16020
16021**Type**: String (path)
16022**Description**: Path to CA file for self-signed certificates
16023
16024**Example**:
16025\`\`\`bash
16026--localCa="./certs/ca.crt"
16027\`\`\`
16028
16029---
16030
16031#### \`--allowInvalidCert\`
16032
16033**Type**: Boolean
16034**Description**: Ignore SSL certificate errors
16035**Default**: false
16036
16037**Example**:
16038\`\`\`bash
16039--allowInvalidCert
16040\`\`\`
16041
16042---
16043
16044#### \`--proxyAllUrls\`
16045
16046**Type**: Boolean
16047**Description**: Proxy all URLs through tunnel
16048**Default**: false
16049
16050---
16051
16052#### \`--rewriteHostnameToAppUrl\`
16053
16054**Type**: Boolean
16055**Description**: Rewrite request hostnames
16056**Default**: false
16057
16058---
16059
16060#### \`--enableDnsCache\`
16061
16062**Type**: Boolean
16063**Description**: Enable DNS caching
16064**Default**: false
16065
16066---
16067
16068#### \`--printRequests\`
16069
16070**Type**: Boolean
16071**Description**: Log all requests through tunnel
16072**Default**: false
16073
16074**Example**:
16075\`\`\`bash
16076--printRequests
16077\`\`\`
16078
16079---
16080
16081#### \`--http2Connections\`
16082
16083**Type**: Number
16084**Description**: Number of HTTP/2 connections for multiplexing
16085**Default**: Number of CPU cores
16086
16087**Example**:
16088\`\`\`bash
16089--http2Connections=8
16090\`\`\`
16091
16092---
16093
16094### Complete Example
16095
16096\`\`\`bash
16097# Basic tunnel
16098npx @alwaysmeticulous/cli ci start-tunnel \\
16099  --port=3000
16100
16101# With request logging
16102npx @alwaysmeticulous/cli ci start-tunnel \\
16103  --port=3000 \\
16104  --printRequests
16105
16106# HTTPS tunnel with self-signed cert
16107npx @alwaysmeticulous/cli ci start-tunnel \\
16108  --port=3000 \\
16109  --localHttps \\
16110  --allowInvalidCert
16111
16112# Multi-server setup
16113npx @alwaysmeticulous/cli ci start-tunnel \\
16114  --port=3000 \\
16115  --proxyAllUrls
16116\`\`\`
16117
16118---
16119
16120### Output
16121
16122When tunnel starts successfully:
16123
16124\`\`\`
16125Your url is: https://abc123.meticulous.ai
16126user: meticulous, password: ******
16127\`\`\`
16128
16129Use these credentials to access your app through the tunnel.
16130
16131---
16132
16133## ci prepare
16134
16135Ensure a base test run exists before running tests.
16136
16137### Synopsis
16138
16139\`\`\`bash
16140npx @alwaysmeticulous/cli ci prepare \\
16141  --apiToken="<token>" \\
16142  [options]
16143\`\`\`
16144
16145### Required Flags
16146
16147#### \`--apiToken\`
16148
16149**Type**: String
16150**Description**: Your Meticulous API token
16151
16152---
16153
16154### Required Flags
16155
16156#### \`--triggerScript\`
16157
16158**Type**: String
16159**Description**: Path to script that triggers a test run on a specific commit
16160
16161---
16162
16163### Optional Flags
16164
16165#### \`--headCommit\`
16166
16167**Type**: String
16168**Description**: Commit SHA to check/prepare
16169**Default**: Auto-detected
16170
16171---
16172
16173### Complete Example
16174
16175\`\`\`bash
16176npx @alwaysmeticulous/cli ci prepare \\
16177  --apiToken="$METICULOUS_API_TOKEN" \\
16178  --triggerScript="./scripts/trigger-test-run.sh"
16179\`\`\`
16180
16181---
16182
16183## ci label-commit
16184
16185Attach labels to a commit. Labelling a commit as \`not-relevant\` tells Meticulous the commit doesn't affect the app under test, so it can be skipped when searching for a base test run to compare against.
16186
16187### Synopsis
16188
16189\`\`\`bash
16190npx @alwaysmeticulous/cli ci label-commit \\
16191  --apiToken="<token>" \\
16192  --labels not-relevant \\
16193  [options]
16194\`\`\`
16195
16196### Required Flags
16197
16198#### \`--apiToken\`
16199
16200**Type**: String
16201**Description**: Your Meticulous API token
16202
16203---
16204
16205#### \`--labels\`
16206
16207**Type**: String (list)
16208**Description**: The labels to attach to the commit. Supported labels: \`not-relevant\`
16209
16210---
16211
16212### Optional Flags
16213
16214#### \`--commitSha\`
16215
16216**Type**: String
16217**Description**: The commit to label
16218**Default**: Auto-detected from git
16219
16220---
16221
16222### Complete Example
16223
16224\`\`\`bash
16225npx @alwaysmeticulous/cli ci label-commit \\
16226  --apiToken="$METICULOUS_API_TOKEN" \\
16227  --labels not-relevant
16228\`\`\`
16229
16230---
16231
16232## Common Patterns
16233
16234### Environment Variables
16235
16236Set API token via environment variable:
16237
16238\`\`\`bash
16239export METICULOUS_API_TOKEN="met_live_abc123..."
16240
16241# Now can omit --apiToken flag
16242npx @alwaysmeticulous/cli ci run-with-tunnel \\
16243  --appUrl="http://localhost:3000"
16244\`\`\`
16245
16246---
16247
16248### CI Integration
16249
16250#### GitHub Actions
16251
16252\`\`\`yaml
16253- name: Run Meticulous tests
16254  run: |
16255    npx @alwaysmeticulous/cli ci run-with-tunnel \\
16256      --apiToken="\${{ secrets.METICULOUS_API_TOKEN }}" \\
16257      --appUrl="http://localhost:3000"
16258\`\`\`
16259
16260#### GitLab CI
16261
16262\`\`\`yaml
16263script:
16264  - >
16265    npx @alwaysmeticulous/cli ci upload-assets
16266    --apiToken="$METICULOUS_API_TOKEN"
16267    --appDirectory="dist"
16268    --commitSha="$CI_COMMIT_SHA"
16269\`\`\`
16270
16271---
16272
16273### Debug Mode
16274
16275Enable verbose logging:
16276
16277\`\`\`bash
16278DEBUG=meticulous:* npx @alwaysmeticulous/cli ci run-with-tunnel \\
16279  --apiToken="$METICULOUS_API_TOKEN" \\
16280  --appUrl="http://localhost:3000"
16281\`\`\`
16282
16283---
16284
16285### Scripting
16286
16287Use in shell scripts:
16288
16289\`\`\`bash
16290#!/bin/bash
16291set -e
16292
16293# Start app
16294npm start &
16295APP_PID=$!
16296
16297# Wait for app
16298npx wait-on http://localhost:3000
16299
16300# Run tests
16301npx @alwaysmeticulous/cli ci run-with-tunnel \\
16302  --apiToken="$METICULOUS_API_TOKEN" \\
16303  --appUrl="http://localhost:3000"
16304
16305# Cleanup
16306kill $APP_PID
16307\`\`\`
16308
16309---
16310
16311### Machine-readable output
16312
16313\`ci upload-assets\`, \`ci upload-container\`, and \`ci run-with-uploaded-asset-chunks\` accept \`--json\`. With it, the command prints exactly one JSON object on stdout describing the outcome. Progress, notices, and errors go to stderr, so stdout can be parsed directly. Exit codes are the same with or without \`--json\`.
16314
16315\`--json\` hides progress logs by default. To see them as well, pass \`--logLevel info\` (or \`debug\`); they are written to stderr and stdout still carries only the JSON.
16316
16317The result is one of three shapes, told apart by \`outcome\`:
16318
16319\`\`\`json
16320{
16321  "cliVersion": "x.y.z",
16322  "outcome": "success",
16323  "testRunId": "…",
16324  "status": null,
16325  "sourceDeploymentId": "…",
16326  "testRunUrl": "https://app.meticulous.ai/projects/<org>/<project>/test-runs/<testRunId>"
16327}
16328\`\`\`
16329
16330\`\`\`json
16331{
16332  "cliVersion": "x.y.z",
16333  "outcome": "skipped",
16334  "reason": "comments_disabled_for_author",
16335  "message": "Test run skipped because CI comments and checks are disabled for this pull request author.",
16336  "testRunId": null,
16337  "status": null,
16338  "sourceDeploymentId": "…"
16339}
16340\`\`\`
16341
16342\`\`\`json
16343{
16344  "cliVersion": "x.y.z",
16345  "outcome": "failed",
16346  "reason": "remote",
16347  "message": "…"
16348}
16349\`\`\`
16350
16351| Field | Present | Meaning |
16352|-------|---------|---------|
16353| \`cliVersion\` | Always | Version of \`@alwaysmeticulous/cli\` that produced the result |
16354| \`outcome\` | Always | \`success\`, \`skipped\`, or \`failed\` |
16355| \`reason\` | \`skipped\` and \`failed\` | Why the run was skipped or failed (see below) |
16356| \`message\` | \`skipped\` and \`failed\` | Human-readable explanation |
16357| \`testRunId\` | \`success\` and \`skipped\`, and \`failed\` once a test run exists | ID of the test run, or \`null\` when none was created |
16358| \`status\` | \`success\` and \`skipped\` | On \`success\` with \`--waitForTestRunToComplete\`, the final test-run status. Otherwise \`null\` |
16359| \`sourceDeploymentId\` | When a deployment was created | ID of the uploaded build. This is not the test run ID |
16360| \`testRunUrl\` | When a test run was created | Link to the test run |
16361
16362\`sourceDeploymentId\` is also included on a \`failed\` result when the upload finished but the test run could not be created. With \`--waitForTestRunToComplete\`, a run that ends as \`Aborted\` or \`ExecutionError\`, or does not finish within 10 minutes, gives a \`failed\` result that still includes \`testRunId\`, \`testRunUrl\`, and \`sourceDeploymentId\`.
16363
16364Skip reasons: \`comments_disabled_for_author\` (CI comments and checks are disabled for the pull request author; the build is still uploaded), \`all_sessions_excluded\` (\`--sessionFilter\` matched no sessions), \`nothing_to_test\` (base and head are the same commit with no diff), and \`dry_run\`.
16365
16366Failure reasons: \`usage\` (invalid arguments), \`auth\` (the API token was rejected), \`environment\` (a local file or directory is missing or unreadable), \`cli_out_of_date\` (upgrade the CLI), \`remote\` (the Met
16366iculous API returned an error), and \`unexpected\`.
16367
16368A \`comments_disabled_for_author\` skip exits with code 0.
16369
16370Upload sizes and part-by-part progress are not included in the JSON. Pass \`--logLevel info\` to get them on stderr.
16371
16372---
16373
16374## Troubleshooting
16375
16376### "API token required"
16377
16378**Cause**: No API token provided
16379
16380**Solution**: Pass \`--apiToken\` or set \`METICULOUS_API_TOKEN\` env var
16381
16382---
16383
16384### "Failed to connect"
16385
16386**Cause**: App not running or wrong URL
16387
16388**Solutions**:
163891. Verify app is running: \`curl http://localhost:3000\`
163902. Check port in \`--appUrl\` matches actual port
163913. Increase wait time before running command
16392
16393---
16394
16395### "No sessions found"
16396
16397**Cause**: No recorded sessions for project
16398
16399**Solution**: Record sessions first (add recorder snippet to app)
16400
16401---
16402
16403### "Tunnel connection failed"
16404
16405**Cause**: Network/firewall issue
16406
16407**Solutions**:
164081. Check outbound HTTPS (443) is allowed
164092. Try \`--printRequests\` to debug
164103. Contact support if persists
16411
16412---
16413
16414## See Also
16415
16416- [GitHub Actions Setup](${o.GITHUB_ACTIONS_SETUP_URL}) - GitHub Actions configuration
16417- [Tunnel Advanced Options](${o.TUNNEL_ADVANCED_OPTIONS_URL}) - Detailed tunnel configuration
16418- [FAQ & Troubleshooting](${o.FAQ_AND_TROUBLESHOOTING_URL}) - Common issues and solutions
16419`},{id:"environment-variables",url:"/docs/reference/environment-variables",document:`---
16420{
16421  "title": "Environment Variables Reference"
16422}
16423---
16424
16425# {% $frontmatter.title %}
16426
16427Reference for environment variables that configure Meticulous replay behavior. These are primarily useful when running [local simulations](${o.DETECT_DIFFS_LOCALLY_URL}) or debugging replay issues.
16428
16429Set these in your shell before running [CLI commands](${o.CLI_COMMANDS_URL}):
16430
16431\`\`\`bash
16432METICULOUS_HOLD_BROWSER_OPEN=true npx @alwaysmeticulous/cli simulate \\
16433  --sessionId="<id>" --appUrl="<url>"
16434\`\`\`
16435
16436---
16437
16438## Debugging & Inspection
16439
16440### \`METICULOUS_HOLD_BROWSER_OPEN\`
16441
16442**Type**: Boolean (\`true\`/\`false\`)
16443
16444Keep the browser open after a replay completes so you can inspect the final state, open DevTools, and explore the DOM.
16445
16446\`\`\`bash
16447METICULOUS_HOLD_BROWSER_OPEN=true npx @alwaysmeticulous/cli simulate \\
16448  --sessionId="<id>" --appUrl="<url>"
16449\`\`\`
16450
16451---
16452
16453### \`METICULOUS_SHOW_MOUSE_LOCATION\`
16454
16455**Type**: Boolean (\`true\`/\`false\`)
16456
16457Displays the mouse position as a red dot on the page during replay. Helpful for verifying that mouse events are targeting the correct elements.
16458
16459---
16460
16461### \`METICULOUS_TRACK_UNEXPECTED_EXECUTION\`
16462
16463**Type**: Boolean (\`true\`/\`false\`)
16464
16465Enables tracking and pausing on unexpected JavaScript execution (code running outside of the expected replay timeline). When the replay encounters unexpected execution it will pause in the Chromium debugger.
16466
16467**Tips**:
16468- Run \`new Error().stack\` in the console when paused to get a stack trace
16469- In the Chromium debugger, tick "Show ignore-listed frames" when viewing stack traces
16470
16471> **Note**: This must be enabled for \`METICULOUS_UNEXPECTED_EXECUTION_AUTO_RESUME\` and \`METICULOUS_SET_BREAKPOINTS\` to take effect.
16472
16473---
16474
16475### \`METICULOUS_UNEXPECTED_EXECUTION_AUTO_RESUME\`
16476
16477**Type**: Boolean (\`true\`/\`false\`)
16478
16479Automatically resumes when unexpected execution is encountered instead of pausing. Use this when you want to see the logs without manually stepping through each pause.
16480
16481Requires \`METICULOUS_TRACK_UNEXPECTED_EXECUTION=true\`.
16482
16483---
16484
16485### \`METICULOUS_SET_BREAKPOINTS\`
16486
16487**Type**: JSON string
16488
16489Set breakpoints as a JSON array. You can copy breakpoint strings from the logs when \`METICULOUS_TRACK_UNEXPECTED_EXECUTION\` is enabled.
16490
16491Breakpoints can be specified as objects:
16492
16493\`\`\`bash
16494METICULOUS_SET_BREAKPOINTS='[{"scriptId": "<id>", "lineNumber": 10, "columnNumber": 5}]'
16495\`\`\`
16496
16497Or as URL strings:
16498
16499\`\`\`bash
16500METICULOUS_SET_BREAKPOINTS='["https://example.com/script.js:10:5"]'
16501\`\`\`
16502
16503---
16504
16505### \`METICULOUS_DEBUG_DOM_UPDATES\`
16506
16507**Type**: Boolean (\`true\`/\`false\`)
16508
16509Logs details of DOM mutations that occur at unexpected times (e.g. outside of \`advanceVirtualTime\`), including the HTML of the mutated elements.
16510
16511---
16512
16513### \`METICULOUS_PAUSE_BEFORE_REDIRECT\`
16514
16515**Type**: String (URL fragment)
16516
16517Pauses the browser before any redirects whose URL contains the specified fragment. For example, to pause before redirecting to a login page:
16518
16519\`\`\`bash
16520METICULOUS_PAUSE_BEFORE_REDIRECT=login
16521\`\`\`
16522
16523---
16524
16525## Timing & Timeouts
16526
16527### \`METICULOUS_NO_TIMEOUT\`
16528
16529**Type**: Boolean (\`true\`/\`false\`)
16530
16531Disables all timeouts except for the navigation timeout (which is extended to 60 minutes). Useful when pausing in debuggers or stepping through replay execution.
16532
16533---
16534
16535### \`METICULOUS_REPLAY_TIMEOUT_MINUTES\`
16536
16537**Type**: Number (minutes)
16538
16539Sets the replay timeout to the specified number of minutes, overriding the default.
16540
16541\`\`\`bash
16542METICULOUS_REPLAY_TIMEOUT_MINUTES=10
16543\`\`\`
16544
16545---
16546
16547### \`METICULOUS_MAX_DURATION_MS\`
16548
16549**Type**: Number (milliseconds)
16550
16551Cuts replays short when they reach the specified virtual time. Useful when running a test run and you only want to replay the first portion of each session.
16552
16553\`\`\`bash
16554METICULOUS_MAX_DURATION_MS=30000
16555\`\`\`
16556
16557---
16558
16559## Browser Configuration
16560
16561### \`METICULOUS_ADDITIONAL_CHROMIUM_FLAGS\`
16562
16563**Type**: String (comma-separated flags)
16564
16565Passes additional flags to the Chromium browser instance launched for replay.
16566
16567\`\`\`bash
16568METICULOUS_ADDITIONAL_CHROMIUM_FLAGS="--disable-gpu,--no-sandbox"
16569\`\`\`
16570
16571
16572---
16573
16574## Feature Toggles
16575
16576### \`METICULOUS_DISABLE_SENTRY\`
16577
16578**Type**: Boolean (\`true\`/\`false\`)
16579
16580Disables the customer application's Sentry initialization during replay. This simplifies stack traces and reduces noise when debugging replay issues.
16581
16582---
16583
16584### \`METICULOUS_DISABLE_RECAPTCHA\`
16585
16586**Type**: Boolean (\`true\`/\`false\`)
16587
16588Disables Google reCAPTCHA script loading during replay. This simplifies stack traces and reduces noise when debugging.
16589
16590---
16591
16592### \`METICULOUS_DISABLE_WEB_WORKERS\`
16593
16594**Type**: Boolean (\`true\`/\`false\`)
16595
16596Disables Web Workers (\`window.Worker\`) during replay. Useful if workers are causing replay issues. Leave disabled (or set project setting \`disableWebWorkers: false\`) when using dedicated worker network recording.
16597
16598---
16599
16600### \`METICULOUS_DISABLE_SHARED_WORKERS\`
16601
16602**Type**: Boolean (\`true\`/\`false\`)
16603
16604Disables Shared Workers (\`window.SharedWorker\`) during replay.
16605
16606
16607`},{id:"performance-api",url:"/docs/reference/performance-api",document:`---
16608{
16609  "title": "Performance API Reference"
16610}
16611---
16612
16613# {% $frontmatter.title %}
16614
16615{% callout_card variant="info" title="Actively under development" %}
16616The Performance API is under active development and its surface may evolve. If you're interested in using it or have feedback, we'd love to hear from you — reach out at [[email protected]](mailto:[email protected]).
16617{% /callout_card %}
16618
16619Meticulous can feed frontend performance data into your existing monitoring systems — such as Datadog, Grafana, New Relic, or any custom analytics pipeline — so you can track how rendering speed, JavaScript execution time, and memory usage change across commits.
16620
16621When Meticulous replays a recorded session, it stubs browser time and performance APIs to ensure deterministic behavior. The Performance API provides access to **real, non-stubbed** browser performance primitives that you can report directly to your monitoring backend.
16622
16623---
16624
16625## Overview
16626
16627During a Meticulous replay:
16628
16629- \`performance.now()\`, \`Date.now()\`, and other timing APIs return **virtual** (deterministic) values.
16630- \`performance.memory\` returns **fixed** values.
16631- \`performance.measureUserAgentSpecificMemory()\` returns **fixed** values.
16632- \`PerformanceObserver\` is stubbed.
16633- \`PressureObserver\` is stubbed.
16634- \`setTimeout\`, \`setInterval\`, \`clearTimeout\`, and \`clearInterval\` schedule callbacks on **virtual** (deterministic) time.
16635
16636The Performance API, available on \`window.Meticulous.replay.native\`, bypasses this stubbing and returns **real** values. This lets you measure actual frontend performance and feed it into dashboards like Datadog, Grafana, or your own monitoring system.
16637
16638{% callout_card variant="warning" title="Frontend performance only" %}
16639This API measures **frontend performance only**: rendering time, JavaScript execution, and memory usage.
16640
16641During replays all network traffic is mocked using previously recorded responses — network requests return instantly with cached data. **Do not use these APIs to measure network latency, API response times, or backend performance.** Those measurements are not meaningful during a replay.
16642{% /callout_card %}
16643
16644---
16645
16646## When to Use
16647
16648Only collect and report metrics when \`isBenchmarkableReplay\` is \`true\`. This flag indicates the replay was executed under conditions where performance data is meaningful.
16649
16650\`\`\`typescript
16651if (window.Meticulous?.replay?.isBenchmarkableReplay) {
16652  // Safe to collect and report performance metrics
16653}
16654\`\`\`
16655
16656---
16657
16658## Available APIs
16659
16660All APIs live on \`window.Meticulous.replay.native\`.
16661
16662### \`native.performance.now()\`
16663
16664Returns the real elapsed time in milliseconds, bypassing Meticulous's virtual time. Wraps the native [\`Performance.now()\`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now).
16665
16666**Returns**: \`number\`
16667
16668\`\`\`typescript
16669const realElapsed = window.Meticulous.replay.native.performance.now();
16670\`\`\`
16671
16672Use this wherever you would normally use \`performance.now()\` to measure durations:
16673
16674\`\`\`typescript
16675const perf = window.Meticulous?.replay?.isBenchmarkableReplay
16676  ? window.Meticulous.replay.native.performance
16677  : window.performance;
16678
16679const start = perf.now();
16680doExpensiveWork();
16681const duration = perf.now() - start;
16682\`\`\`
16683
16684---
16685
16686### \`native.performance.memory\`
16687
16688**Type**: \`{ jsHeapSizeLimit: number; totalJSHeapSize: number; usedJSHeapSize: number } | undefined\`
16689
16690Returns actual browser memory usage, bypassing the fixed values Meticulous stubs in. Wraps the native [\`Performance.memory\`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory).
16691
16692| Property | Type | Description |
16693|----------|------|-------------|
16694| \`jsHeapSizeLimit\` | \`number\` | Maximum heap size in bytes |
16695| \`totalJSHeapSize\` | \`number\` | Total allocated heap in bytes |
16696| \`usedJSHeapSize\` | \`number\` | Currently used heap in bytes |
16697
16698\`\`\`typescript
16699const mem = window.Meticulous.replay.native.performance.memory;
16700if (mem) {
16701  console.log("Heap used:", mem.usedJSHeapSize);
16702}
16703\`\`\`
16704
16705---
16706
16707### \`native.performance.measureUserAgentSpecificMemory()\`
16708
16709**Type**: \`(() => Promise<MemoryMeasurement>) | undefined\`
16710
16711Returns a promise that resolves to a detailed, attributed breakdown of the memory used by all JavaScript realms (the page, its iframes, and its workers), bypassing Meticulous's deterministic stub. Wraps the native [\`Performance.measureUserAgentSpecificMemory()\`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory).
16712
16713The resolved value has the following shape:
16714
16715| Property | Type | Description |
16716|----------|------|-------------|
16717| \`bytes\` | \`number\` | Total memory used, in bytes |
16718| \`breakdown\` | \`Array<{ bytes: number; attribution: unknown[]; types: string[] }>\` | Per-realm/per-type breakdown of \`bytes\` |
16719
16720\`breakdown[].types\` is an open-ended set that varies by browser, build, and the page itself (e.g. \`"JavaScript"\`, \`"DOM"\`, \`"Shared"\`, \`"WebAssembly"\`, \`"Detached"\`, ...). A single breakdown entry can carry multiple types, so per-type buckets generally do not sum to \`bytes\` — use \`bytes\` for the canonical total.
16721
16722\`\`\`typescript
16723const measure = window.Meticulous.replay.native.performance
16724  .measureUserAgentSpecificMemory;
16725
16726if (measure) {
16727  const measurement = await measure();
16728  console.log("Total bytes:", measurement.bytes);
16729  for (const entry of measurement.breakdown) {
16730    console.log(entry.types, entry.bytes);
16731  }
16732}
16733\`\`\`
16734
16735{% callout_card variant="warning" title="Cross-origin isolation and availability" %}
16736The native \`measureUserAgentSpecificMemory()\` is normally only callable from a [cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) context. **Meticulous replays do not run in a cross-origin isolated context**, so this API may be \`undefined\` (or throw a \`SecurityError\` when called) and you should always check for its presence and \`try/catch\` around the call.
16737
16738However, depending on how Meticulous proxies and serves your application during replay, the underlying browser context may end up satisfying the security checks even when \`window.crossOriginIsolated\` is \`false\`. As a result, on some projects this function is callable and returns real measurements. Contact the Meticulous engineering team for more information about whether this API is available for your project.
16739{% /callout_card %}
16740
16741---
16742
16743### \`native.PerformanceObserver\`
16744
16745**Type**: \`typeof PerformanceObserver\`
16746
16747The native [\`PerformanceObserver\`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver) constructor for observing real performance entries (e.g., \`navigation\`, \`resource\`, \`measure\`), unaffected by virtual time.
16748
16749\`\`\`typescript
16750const observer = new window.Meticulous.replay.native.PerformanceObserver(
16751  (list) => {
16752    for (const entry of list.getEntries()) {
16753      reportMetric(entry.name, entry.duration);
16754    }
16755  }
16756);
16757observer.observe({ entryTypes: ["measure", "navigation"] });
16758\`\`\`
16759
16760---
16761
16762### \`native.PressureObserver\`
16763
16764**Type**: \`typeof PressureObserver | undefined\`
16765
16766The native [\`PressureObserver\`](https://developer.mozilla.org/en-US/docs/Web/API/PressureObserver) constructor from the [Compute Pressure API](https://developer.mozilla.org/en-US/docs/Web/API/Compute_Pressure_API), exposed under \`native\` for symmetry with the other performance primitives.
16767
16768Use it to observe the pressure state of system resources such as the CPU (\`"nominal"\`, \`"fair"\`, \`"serious"\`, \`"critical"\`) and correlate it with your frontend performance metrics.
16769
16770\`\`\`typescript
16771const replay = window.Meticulous?.replay;
16772const PressureObserver = replay?.native.PressureObserver;
16773
16774if (replay?.isBenchmarkableReplay && PressureObserver) {
16775  const observer = new PressureObserver((records) => {
16776    for (const record of records) {
16777      reportPressure({
16778        source: record.source,
16779        state: record.state,
16780        time: record.time,
16781      });
16782    }
16783  });
16784
16785  observer.observe("cpu", { sampleInterval: 1000 }).catch((err) => {
16786    console.warn("Failed to observe CPU pressure", err);
16787  });
16788}
16789\`\`\`
16790
16791The callback receives \`MeticulousPressureRecord[]\` entries, each with a \`source\` (\`MeticulousPressureSource\`, currently \`"cpu"\`), a \`state\` (\`MeticulousPressureState\`), and a \`time\` (\`DOMHighResTimeStamp\`). Types are exported from [\`@alwaysmeticulous/replay-browser-scripts-api\`](${o.TYPESCRIPT_TYPES_URL}) as \`MeticulousPressureObserver\`, \`MeticulousPressureObserverConstructor\`, \`MeticulousPressureRecord\`, \`MeticulousPressureSource\`, and \`MeticulousPressureState\`.
16792
16793---
16794
16795### \`native.setTimeout()\`, \`native.setInterval()\`, \`native.clearTimeout()\`, and \`native.clearInterval()\`
16796
16797**Types**: Same signatures as the native [\`setTimeout\`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout), [\`setInterval\`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval), [\`clearTimeout\`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout), and [\`clearInterval\`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) functions.
16798
16799During a replay, the global \`setTimeout\` and \`setInterval\` are intercepted and schedule callbacks against Meticulous's virtual time. The versions on \`window.Meticulous.replay.native\` use the **real** browser timer functions instead, so callbacks fire after actual wall-clock delays.
16800
16801Use these when you need real-time scheduling — for example, to defer performance metric collection until after the main thread has settled:
16802
16803\`\`\`typescript
16804const replay = window.Meticulous?.replay;
16805if (replay?.isBenchmarkableReplay) {
16806  replay.native.setTimeout(() => {
16807    reportPerformanceMetrics();
16808  }, 0);
16809}
16810\`\`\`
16811
16812{% callout_card variant="warning" title="Use with extreme care" %}
16813These functions run against **real wall-clock time** and their callbacks are **not** synchronized with Meticulous's virtual event loop or screenshot timing.
16814
16815**Avoid any callback behaviour that has a visual impact** — including DOM mutations, CSS changes, animations, scrolls, focus changes, toasts, modals, or any other UI updates. Such side effects can desynchronize the replay from its recorded state and cause visual-test failures or flaky results.
16816
16817Restrict callbacks to **non-visual** work such as collecting metrics, sending analytics payloads, or other read-only instrumentation. Never use these timers to drive UI updates, lazy-load visible content, or trigger layout during a replay.
16818{% /callout_card %}
16819
16820---
16821
16822## Metadata
16823
16824When reporting metrics you'll typically want to attach context about what is being tested. Two properties on \`window.Meticulous.replay\` provide this:
16825
16826### \`commitUnderTest\`
16827
16828**Type**: \`{ sha: string; baseCommitSha: string | null; branchName: string | null; date: string | null } | undefined\`
16829
16830| Property | Type | Description |
16831|----------|------|-------------|
16832| \`sha\` | \`string\` | Full commit SHA being tested |
16833| \`baseCommitSha\` | \`string \\| null\` | Base commit SHA this test run is compared against (typically the commit on the main branch that the PR branch forked from). \`null\` when the test run is not associated with a PR, or when no base commit was specified or could be determined |
16834| \`branchName\` | \`string \\| null\` | Git branch name |
16835| \`date\` | \`string \\| null\` | Commit date in ISO 8601 format |
16836
16837### \`sessionBeingReplayed\`
16838
16839**Type**: \`{ id: string }\`
16840
16841The ID of the recorded session being replayed.
16842
16843### \`browser\`
16844
16845**Type**: \`{ version: string }\`
16846
16847Information about the Chrome/Chromium build that is driving the replay.
16848Useful for tagging reported metrics so that performance dashboards can be
16849sliced by browser version — rendering speed, JavaScript execution time, and
16850memory usage can shift meaningfully between Chrome releases, and aggregating
16851across versions can mask regressions.
16852
16853| Property | Type | Description |
16854|----------|------|-------------|
16855| \`version\` | \`string\` | The Chrome/Chromium version, e.g. \`"139.0.7258.5"\`. The leading product label (\`"Chrome/"\`, \`"HeadlessChrome/"\`, ...) is stripped, so the value can be parsed directly as a dotted version number. Falls back to \`"unknown"\` only in the very rare case Meticulous could not read the version from the underlying browser. |
16856
16857\`\`\`typescript
16858const replay = window.Meticulous?.replay;
16859if (replay) {
16860  console.log("Replay running on Chrome", replay.browser.version);
16861}
16862\`\`\`
16863
16864---
16865
16866## Sending Data to Analytics
16867
16868During replays, Meticulous intercepts and mocks network requests. To let your analytics requests pass through, add the \`meticulous-passthrough\` header set to \`"true"\`:
16869
16870\`\`\`typescript
16871fetch("https://analytics.example.com/metrics", {
16872  method: "POST",
16873  headers: {
16874    "Content-Type": "application/json",
16875    "meticulous-passthrough": "true",
16876  },
16877  body: JSON.stringify(payload),
16878});
16879\`\`\`
16880
16881Without this header, the request will be intercepted by the network stubbing layer and will not reach your analytics endpoint.
16882
16883---
16884
16885## Full Example
16886
16887\`\`\`typescript
16888const reportPerformanceMetrics = () => {
16889  const replay = window.Meticulous?.replay;
16890  if (!replay?.isBenchmarkableReplay) {
16891    return;
16892  }
16893
16894  const elapsed = replay.native.performance.now();
16895  const memory = replay.native.performance.memory;
16896  const commit = replay.commitUnderTest;
16897
16898  fetch("https://analytics.example.com/metrics", {
16899    method: "POST",
16900    headers: {
16901      "Content-Type": "application/json",
16902      "meticulous-passthrough": "true",
16903    },
16904    body: JSON.stringify({
16905      elapsedMs: elapsed,
16906      heapUsedBytes: memory?.usedJSHeapSize,
16907      heapTotalBytes: memory?.totalJSHeapSize,
16908      commitSha: commit?.sha,
16909      baseCommitSha: commit?.baseCommitSha,
16910      branch: commit?.branchName,
16911      sessionId: replay.sessionBeingReplayed.id,
16912      chromeVersion: replay.browser.version,
16913    }),
16914  });
16915};
16916\`\`\`
16917
16918---
16919
16920## Data Noise
16921
16922Treat performance data collected from Meticulous replays as **noisy**. Two factors introduce variance:
16923
16924- **Hardware differences**: Replays may execute on different machines with different CPU, memory, and disk characteristics. Absolute numbers will vary across runs.
16925- **Network mocking overhead**: Meticulous intercepts and mocks all network requests during replay. The mocking mechanism itself introduces some overhead that does not exist in production.
16926
16927Because of this, focus on **trends over time** rather than individual data points. Aggregate across multiple replays and commits to identify meaningful regressions or improvements.
16928
16929---
16930
16931## Requirements
16932
16933- **Gate on \`isBenchmarkableReplay\`**: Always check this before collecting metrics. When \`false\`, the replay conditions do not guarantee meaningful numbers and results should be discarded.
16934- **Do not render metrics in the UI**: Displaying real performance values in the DOM will cause visual differences between runs. Report them to external systems only.
16935- **Native timers are for non-visual work only**: \`native.setTimeout\` and \`native.setInterval\` bypass virtual time. Avoid any callback behaviour with a visual impact — DOM updates, animations, scrolls, focus changes, and similar side effects can break visual tests.
16936- **Use the passthrough header**: Without \`meticulous-passthrough: "true"\`, analytics requests will be intercepted and mocked, and your data will never reach your analytics endpoint.
16937- **Frontend metrics only**: Rendering, JavaScript execution, and memory are meaningful. Network and backend latency are not, because all requests return mocked responses.
16938
16939---
16940
16941## See Also
16942
16943- [window.Meticulous API](${o.METICULOUS_WINDOW_OBJECT_URL}) — detecting test mode, pause/resume, custom data
16944- [TypeScript Types](${o.TYPESCRIPT_TYPES_URL}) — importable type definitions for \`window.Meticulous\`
16945`}];e.s(["DOC_REGISTRY",0,tp,"getDocumentByDocsUrl",0,e=>tp.find(t=>t.url===e)?.document],88865)},828220,452290,727685,e=>{"use strict";var t=e.i(318008),s=e.i(35203),o=e.i(687652),n=e.i(761947),i=e.i(837991),a=e.i(917910);let r=e.i(88865).DOC_REGISTRY.map(({id:e,url:t,document:s})=>(0,a.processDocument)(e,t,s)),l=(0,o.createContext)(null);function c(e,t){return t[1]-e[1]}function u(e){let[t]=e;return t}function d(e){return null!==e}e.s(["SearchProvider",0,e=>{let a,h,p,m,f,g,y,w,b,v=(0,s.c)(15),{children:k}=e,[S,T]=(0,o.useState)("");v[0]===Symbol.for("react.memo_cache_sentinel")?(a=[],v[0]=a):a=v[0];let[_,I]=(0,o.useState)(a),[R,C]=(0,o.useState)(!1);if(v[1]===Symbol.for("react.memo_cache_sentinel")){let e=new n.default.Index({tokenize:"forward",context:!0}),t=new n.default.Index({tokenize:"forward",context:!0}),s=new n.default.Index({tokenize:"forward",context:!0});r.forEach(o=>{e.add(o.id,o.title),t.add(o.id,o.headings.join(" ")),s.add(o.id,o.content)}),h={titleIndex:e,headingsIndex:t,contentIndex:s},v[1]=h}else h=v[1];let{titleIndex:x,headingsIndex:A,contentIndex:M}=h;v[2]===Symbol.for("react.memo_cache_sentinel")?(p=e=>{if(T(e),!e.trim())return void I([]);try{let t=x.search(e,{limit:50}),s=A.search(e,{limit:50}),o=M.search(e,{limit:50}),n=new Map;t.forEach(e=>{n.set(e,(n.get(e)||0)+3)}),s.forEach(e=>{n.set(e,(n.get(e)||0)+2)}),o.forEach(e=>{n.set(e,(n.get(e)||0)+1)});let i=Array.from(n.entries()).sort(c).map(u).slice(0,20).map(t=>{let s=r.find(e=>e.id===t);if(!s)return null;let o=((e,t)=>{let s=t.toLowerCase(),o=e.toLowerCase().indexOf(s);if(-1===o)return e.slice(0,150)+(e.length>150?"...":"");let n=Math.max(0,o-Math.floor(75)),i=Math.min(e.length,n+150),a=e.slice(n,i);return n>0&&(a="..."+a),i<e.length&&(a+="..."),a.trim()})(s.content,e);return{id:s.id,title:s.title,url:s.url,excerpt:o,headings:s.headings}}).filter(d);I(i)}catch(e){console.error("Search error:",e),I([])}},v[2]=p):p=v[2];let E=p;v[3]===Symbol.for("react.memo_cache_sentinel")?(m=()=>{C(!0)},v[3]=m):m=v[3];let U=m;v[4]===Symbol.for("react.memo_cache_sentinel")?(f=()=>{C(!1),T(""),I([])},v[4]=f):f=v[4];let L=f;v[5]!==R?(g=()=>{let e=e=>{if(((0,i.isMacPlatform)()?e.metaKey:e.ctrlKey)&&"k"===e.key.toLowerCase()){e.preventDefault(),R?L():U();return}"Escape"===e.key&&R&&L()};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},y=[R,L,U],v[5]=R,v[6]=g,v[7]=y):(g=v[6],y=v[7]),(0,o.useEffect)(g,y),v[8]!==R||v[9]!==S||v[10
16945]!==_?(w={query:S,results:_,isOpen:R,search:E,openSearch:U,closeSearch:L},v[8]=R,v[9]=S,v[10]=_,v[11]=w):w=v[11];let P=w;return v[12]!==k||v[13]!==P?(b=(0,t.jsx)(l.Provider,{value:P,children:k}),v[12]=k,v[13]=P,v[14]=b):b=v[14],b},"useSearch",0,()=>{let e=(0,o.useContext)(l);if(!e)throw Error("useSearch must be used within a SearchProvider");return e}],828220);let h=()=>()=>{},p=()=>(0,i.getCommandModifierLabel)(),m=()=>null;e.s(["useCommandModifierLabel",0,()=>(0,o.useSyncExternalStore)(h,p,m)],452290);let f=o.forwardRef(function({title:e,titleId:t,...s},n){return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon",ref:n,"aria-labelledby":t},s),e?o.createElement("title",{id:t},e):null,o.createElement("path",{fillRule:"evenodd",d:"M7.455 2.004a.75.75 0 0 1 .26.77 7 7 0 0 0 9.958 7.967.75.75 0 0 1 1.067.853A8.5 8.5 0 1 1 6.647 1.921a.75.75 0 0 1 .808.083Z",clipRule:"evenodd"}))}),g=o.forwardRef(function({title:e,titleId:t,...s},n){return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon",ref:n,"aria-labelledby":t},s),e?o.createElement("title",{id:t},e):null,o.createElement("path",{d:"M10 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2ZM10 15a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 15ZM10 7a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM15.657 5.404a.75.75 0 1 0-1.06-1.06l-1.061 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM6.464 14.596a.75.75 0 1 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM18 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 18 10ZM5 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 5 10ZM14.596 15.657a.75.75 0 0 0 1.06-1.06l-1.06-1.061a.75.75 0 1 0-1.06 1.06l1.06 1.06ZM5.404 6.464a.75.75 0 0 0 1.06-1.06l-1.06-1.06a.75.75 0 1 0-1.061 1.06l1.06 1.06Z"}))});var y=e.i(944967),w=e.i(136195);e.s(["DocsThemeToggle",0,()=>{let e,o,n=(0,s.c)(2);return n[0]===Symbol.for("react.memo_cache_sentinel")?(e=(0,y.default)("inline-flex","items-center","justify-center","rounded-md","p-2","text-zinc-500","hover:bg-zinc-200/60","hover:text-zinc-900","dark:text-zinc-400","dark:hover:bg-zinc-800","dark:hover:text-zinc-100"),n[0]=e):e=n[0],n[1]===Symbol.for("react.memo_cache_sentinel")?(o=(0,t.jsxs)("button",{type:"button",onClick:w.toggleDocsTheme,className:e,"aria-label":"Toggle dark mode",children:[(0,t.jsx)(f,{className:"h-5 w-5 dark:hidden","aria-hidden":"true"}),(0,t.jsx)(g,{className:"hidden h-5 w-5 dark:block","aria-hidden":"true"})]}),n[1]=o):o=n[1],o}],727685)}]);
16946
16947//# debugId=ca7a8cdf-fb23-afd1-3be2-210be472a154

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.