PageSourceSearch

https://app.meticulous.ai/_next/static/chunks/3ox6ixh_v9_u_.js

js meticulous.ai collected 2026-09-25 13:59:36 UTC 758,636 bytes, 16,795 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]="38892b9f-391b-43fc-ef91-6db054dcdb57")}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_REVIEW_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- Disable analytics/tracking
3929- Skip animations
3930
3931---
3932
3933### recordCustomValues()
3934
3935**Signature**: \`recordCustomValues(values: Record<string, any>): void\`
3936
3937**Description**: Store custom data during recording to be retrieved during replay.
3938
3939**Parameters**:
3940- \`values\`: Object with key-value pairs to store
3941
3942**Size limits**:
3943- **Development**: 20MB total (set \`data-is-production-environment="false"\`)
3944- **Production**: 1MB total
3945
3946**When to use**:
3947- Store file upload contents
3948- Record feature flag values
3949- Save user context
3950- Preserve random/dynamic values
3951
3952**Example**:
3953
3954\`\`\`typescript
3955// During recording
3956const handleFileUpload = (file: File) => {
3957  const reader = new FileReader();
3958  reader.onload = (e) => {
3959    const fileData = e.target?.result as string;
3960
3961    // Store for replay
3962    if (window.Meticulous?.recordCustomValues) {
3963      window.Meticulous.recordCustomValues({
3964        uploadedFile: fileData,
3965        fileName: file.name,
3966        fileType: file.type,
3967      });
3968    }
3969
3970    processFile(fileData);
3971  };
3972  reader.readAsDataURL(file);
3973};
3974\`\`\`
3975
3976**Multiple calls**: Values are merged, not overwritten. Later calls add/update keys.
3977
3978\`\`\`typescript
3979// First call
3980window.Meticulous.recordCustomValues({ key1: 'value1' });
3981
3982// Second call - adds key2, keeps key1
3983window.Meticulous.recordCustomValues({ key2: 'value2' });
3984
3985// Result: { key1: 'value1', key2: 'value2' }
3986\`\`\`
3987
3988---
3989
3990### getCustomValues()
3991
3992**Signature**: \`getCustomValues(): Record<string, any> | undefined\`
3993
3994**Description**: Retrieve custom values stored during recording.
3995
3996**Returns**: Object with stored values, or \`undefined\` if none.
3997
3998**When to use**:
3999- Restore file upload data during replay
4000- Get feature flag values
4001- Restore user context
4002
4003**Example**:
4004
4005\`\`\`typescript
4006// During replay
4007useEffect(() => {
4008  if (window.Meticulous?.isRunningAsTest) {
4009    const stored = window.Meticulous.getCustomValues();
4010
4011    if (stored?.uploadedFile) {
4012      // Restore file preview
4013      setPreview(stored.uploadedFile);
4014    }
4015
4016    if (stored?.featureFlags) {
4017      // Apply feature flags
4018      setFlags(stored.featureFlags);
4019    }
4020  }
4021}, []);
4022\`\`\`
4023
4024**Complete pattern**:
4025
4026\`\`\`typescript
4027function useStoredValue(key: string) {
4028  const [value, setValue] = useState(null);
4029
4030  useEffect(() => {
4031    if (window.Meticulous?.isRunningAsTest) {
4032      const stored = window.Meticulous.getCustomValues();
4033      if (stored?.[key]) {
4034        setValue(stored[key]);
4035      }
4036    }
4037  }, [key]);
4038
4039  return value;
4040}
4041
4042// Usage
4043const userContext = useStoredValue('userContext');
4044\`\`\`
4045
4046---
4047
4048### pause()
4049
4050**Signature**: \`pause(): void\`
4051
4052**Description**: Pause replay until \`resume()\` is called.
4053
4054**When to use**:
4055- Before async operations (data loading, image loading)
4056- Before third-party script initialization
4057- When content loads dynamically
4058
4059**Important**: Always pair with \`resume()\`. Unpaired \`pause()\` will hang the test.
4060
4061**Example - Data Loading**:
4062
4063\`\`\`typescript
4064async function loadCriticalData() {
4065  window.Meticulous?.pause?.();
4066
4067  try {
4068    const data = await fetchDataFromAPI();
4069    setData(data);
4070  } finally {
4071    window.Meticulous?.resume?.();
4072  }
4073}
4074\`\`\`
4075
4076**Example - Image Loading**:
4077
4078\`\`\`typescript
4079function LazyImage({ src }: { src: string }) {
4080  const [loaded, setLoaded] = useState(false);
4081
4082  useEffect(() => {
4083    if (!loaded && window.Meticulous?.isRunningAsTest) {
4084      window.Meticulous?.pause?.();
4085    }
4086  }, [loaded]);
4087
4088  const handleLoad = () => {
4089    setLoaded(true);
4090    window.Meticulous?.resume?.();
4091  };
4092
4093  return <img src={src} onLoad={handleLoad} />;
4094}
4095\`\`\`
4096
4097---
4098
4099### resume()
4100
4101**Signature**: \`resume(): void\`
4102
4103**Description**: Resume replay after \`pause()\`.
4104
4105**When to use**: After the async operation started with \`pause()\` completes.
4106
4107**Example - Third-Party Script**:
4108
4109\`\`\`typescript
4110function loadGoogleMaps() {
4111  return new Promise((resolve) => {
4112    if (window.google?.maps) {
4113      resolve(window.google.maps);
4114      return;
4115    }
4116
4117    window.Meticulous?.pause?.();
4118
4119    const script = document.createElement('script');
4120    script.src = 'https://maps.googleapis.com/.../js?key=KEY';
4121    script.onload = () => {
4122      window.Meticulous?.resume?.();
4123      resolve(window.google.maps);
4124    };
4125    script.onerror = () => {
4126      window.Meticulous?.resume?.();
4127      reject(new Error('Failed to load'));
4128    };
4129
4130    document.head.appendChild(script);
4131  });
4132}
4133\`\`\`
4134
4135---
4136
4137### recordCustomEvent()
4138
4139**Signature**: \`recordCustomEvent(event: { type: string; payload?: any }): void\`
4140
4141**Description**: Record a custom event with optional payload.
4142
4143**Parameters**:
4144- \`event.type\`: String identifying the event type
4145- \`event.payload\`: Optional data to store with event
4146
4147**When to use**: Advanced scenarios requiring fine-grained replay control.
4148
4149**Example**:
4150
4151\`\`\`typescript
4152// Record event during session
4153function handlePaymentComplete(transaction: Transaction) {
4154  if (window.Meticulous?.recordCustomEvent) {
4155    window.Meticulous.recordCustomEvent({
4156      type: 'PAYMENT_COMPLETE',
4157      payload: {
4158        transactionId: transaction.id,
4159        amount: transaction.amount,
4160        timestamp: Date.now(),
4161      },
4162    });
4163  }
4164
4165  showSuccessMessage();
4166}
4167\`\`\`
4168
4169---
4170
4171### onReplayCustomEvent()
4172
4173**Signature**: \`onReplayCustomEvent(handler: (event: CustomEvent) => void): void\`
4174
4175**Description**: Register handler for custom events during replay.
4176
4177**Parameters**:
4178- \`handler\`: Function called when custom event is replayed
4179
4180**Example**:
4181
4182\`\`\`typescript
4183// Setup handler during component mount
4184useEffect(() => {
4185  if (window.Meticulous?.onReplayCustomEvent) {
4186    window.Meticulous.onReplayCustomEvent((event) => {
4187      if (event.type === 'PAYMENT_COMPLETE') {
4188        console.log('Replaying payment:', event.payload);
4189        handlePaymentReplay(event.payload);
4190      }
4191    });
4192  }
4193}, []);
4194\`\`\`
4195
4196---
4197
4198## Backend Detection
4199
4200### meticulous-is-test Header
4201
4202Check for the \`meticulous-is-test\` header in server-side code:
4203
4204**Value**: \`"1"\` when running as test
4205
4206**Security**: For secure detection, set a custom secret header in project settings.
4207
4208### Next.js App Router
4209
4210\`\`\`typescript
4211import { headers } from 'next/headers';
4212
4213export async function isMeticulousTest(): Promise<boolean> {
4214  const requestHeaders = await headers();
4215  return requestHeaders.get('meticulous-is-test') === '1';
4216}
4217
4218// Usage in Server Component
4219export default async function Page() {
4220  const isTest = await isMeticulousTest();
4221
4222  if (isTest) {
4223    // Skip auth, use test data, etc.
4224  }
4225
4226  return <div>...</div>;
4227}
4228\`\`\`
4229
4230### Next.js Pages Router
4231
4232\`\`\`typescript
4233export const getServerSideProps = (context) => {
4234  const { req } = context;
4235  const isTest = req.headers['meticulous-is-test'] === '1';
4236
4237  return {
4238    props: {
4239      isRunningAsMeticulousTest: isTest,
4240    },
4241  };
4242};
4243
4244function Page({ isRunningAsMeticulousTest }) {
4245  if (isRunningAsMeticulousTest) {
4246    // Test-specific rendering
4247  }
4248
4249  return <div>...</div>;
4250}
4251\`\`\`
4252
4253### Express.js
4254
4255\`\`\`typescript
4256app.get('/api/data', (req, res) => {
4257  const isTest = req.headers['meticulous-is-test'] === '1';
4258
4259  if (isTest) {
4260    // Return test data
4261    res.json({ data: 'test-data' });
4262  } else {
4263    // Return real data
4264    res.json({ data: getRealData() });
4265  }
4266});
4267\`\`\`
4268
4269---
4270
4271## Common Patterns
4272
4273### Pattern 1: Skip Validation
4274
4275\`\`\`typescript
4276function submitForm(data: FormData) {
4277  if (!window.Meticulous?.isRunningAsTest) {
4278    // Only validate in normal mode
4279    if (!data.email) {
4280      throw new Error('Email required');
4281    }
4282  }
4283
4284  // Submit form
4285  return api.post('/submit', data);
4286}
4287\`\`\`
4288
4289### Pattern 2: Deterministic Values
4290
4291\`\`\`typescript
4292function generateId(): string {
4293  if (window.Meticulous?.isRunningAsTest) {
4294    return 'test-id-12345';
4295  }
4296  return crypto.randomUUID();
4297}
4298
4299function getCurrentTimestamp(): string {
4300  if (window.Meticulous?.isRunningAsTest) {
4301    return '2024-01-01T00:00:00Z';
4302  }
4303  return new Date().toISOString();
4304}
4305\`\`\`
4306
4307### Pattern 3: Disable Third-Party Services
4308
4309\`\`\`typescript
4310function initializeServices() {
4311  if (!window.Meticulous?.isRunningAsTest) {
4312    initializeAnalytics();
4313    initializeSentry();
4314    initializeIntercom();
4315  }
4316}
4317\`\`\`
4318
4319### Pattern 4: Feature Flags
4320
4321\`\`\`typescript
4322function useFeatureFlag(flagName: string): boolean {
4323  const [enabled, setEnabled] = useState(false);
4324
4325  useEffect(() => {
4326    if (window.Meticulous?.isRunningAsTest) {
4327      const stored = window.Meticulous.getCustomValues();
4328      if (stored?.featureFlags?.[flagName] !== undefined) {
4329        setEnabled(stored.featureFlags[flagName]);
4330        return;
4331      }
4332    }
4333
4334    // Normal flag fetching
4335    fetchFlag(flagName).then(setEnabled);
4336  }, [flagName]);
4337
4338  return enabled;
4339}
4340
4341// Record flags during session
4342if (window.Meticulous?.recordCustomValues) {
4343  window.Meticulous.recordCustomValues({
4344    featureFlags: {
4345      newCheckout: true,
4346      experimentalUI: false,
4347    },
4348  });
4349}
4350\`\`\`
4351
4352### Pattern 5: Conditional Rendering
4353
4354\`\`\`typescript
4355function WelcomeBanner() {
4356  if (window.Meticulous?.isRunningAsTest) {
4357    // Don't show banner during tests
4358    return null;
4359  }
4360
4361  return <div>Welcome!</div>;
4362}
4363\`\`\`
4364
4365---
4366
4367## Integration Patterns
4368
4369### React Hook
4370
4371\`\`\`typescript
4372function useMeticulous() {
4373  return {
4374    isTest: window.Meticulous?.isRunningAsTest ?? false,
4375    recordValues: window.Meticulous?.recordCustomValues,
4376    getValues: window.Meticulous?.getCustomValues,
4377    pause: window.Meticulous?.pause,
4378    resume: window.Meticulous?.resume,
4379  };
4380}
4381
4382// Usage
4383function MyComponent() {
4384  const { isTest, recordValues } = useMeticulous();
4385
4386  if (isTest) {
4387    // Test-specific logic
4388  }
4389}
4390\`\`\`
4391
4392### Context Provider
4393
4394\`\`\`typescript
4395const MeticulousContext = createContext({
4396  isTest: false,
4397  recordValues: () => {},
4398  getValues: () => ({}),
4399});
4400
4401function MeticulousProvider({ children }) {
4402  const value = {
4403    isTest: window.Meticulous?.isRunningAsTest ?? false,
4404    recordValues: window.Meticulous?.recordCustomValues?.bind(window.Meticulous) ?? (() => {}),
4405    getValues: window.Meticulous?.getCustomValues?.bind(window.Meticulous) ?? (() => ({})),
4406  };
4407
4408  return (
4409    <MeticulousContext.Provider value={value}>
4410      {children}
4411    </MeticulousContext.Provider>
4412  );
4413}
4414
4415// Usage
4416const { isTest } = useContext(MeticulousContext);
4417\`\`\`
4418
4419---
4420
4421## Best Practices
4422
4423### 1. Always Use Optional Chaining
4424
4425\`\`\`typescript
4426// Good
4427window.Meticulous?.isRunningAsTest
4428
4429// Bad - throws error if Meticulous not loaded
4430window.Meticulous.isRunningAsTest
4431\`\`\`
4432
4433### 2. Pair pause() with resume()
4434
4435\`\`\`typescript
4436// Good - always resume
4437async function load() {
4438  window.Meticulous?.pause?.();
4439  try {
4440    await fetchData();
4441  } finally {
4442    window.Meticulous?.resume?.();
4443  }
4444}
4445
4446// Bad - might not resume
4447async function load() {
4448  window.Meticulous?.pause?.();
4449  await fetchData();
4450  window.Meticulous?.resume?.(); // Skipped if fetchData throws
4451}
4452\`\`\`
4453
4454### 3. Check isRunningAsTest Before Using Other Methods
4455
4456\`\`\`typescript
4457// Good
4458if (window.Meticulous?.isRunningAsTest) {
4459  const values = window.Meticulous.getCustomValues();
4460}
4461
4462// Also good
4463const values = window.Meticulous?.getCustomValues?.();
4464\`\`\`
4465
4466### 4. Record Early, Retrieve Early
4467
4468\`\`\`typescript
4469// Record as soon as data is available
4470useEffect(() => {
4471  if (userData && window.Meticulous?.recordCustomValues) {
4472    window.Meticulous.recordCustomValues({ user: userData });
4473  }
4474}, [userData]);
4475
4476// Retrieve during component initialization
4477useEffect(() => {
4478  if (window.Meticulous?.isRunningAsTest) {
4479    const stored = window.Meticulous.getCustomValues();
4480    if (stored?.user) {
4481      setUser(stored.user);
4482    }
4483  }
4484}, []); // Empty deps - run once
4485\`\`\`
4486
4487---
4488
4489## Troubleshooting
4490
4491### window.Meticulous is undefined
4492
4493**Cause**: Recorder not loaded
4494
4495**Solutions**:
4496- Verify recorder script tag is in HTML
4497- Check script loads before app code
4498- Check for CSP blocking
4499
4500### Custom values not available during replay
4501
4502**Cause**: Values not recorded or size limit exceeded
4503
4504**Solutions**:
4505- Check \`recordCustomValues\` was called during recording
4506- Verify value size < 1MB (prod) or 20MB (dev)
4507- Check \`data-is-production-environment\` setting
4508
4509### Pause/Resume hanging tests
4510
4511**Cause**: \`resume()\` never called
4512
4513**Solutions**:
4514- Use try/finally to ensure \`resume()\` always runs
4515- Check async callbacks complete
4516- Add timeout fallback
4517
4518---
4519
4520## TypeScript Types
4521
4522For full TypeScript type definitions:
4523
4524\`\`\`typescript
4525interface Meticulous {
4526  isRunningAsTest?: boolean;
4527  recordCustomValues?: (values: Record<string, any>) => void;
4528  getCustomValues?: () => Record<string, any> | undefined;
4529  pause?: () => void;
4530  resume?: () => void;
4531  recordCustomEvent?: (event: { type: string; payload?: any }) => void;
4532  onReplayCustomEvent?: (handler: (event: any) => void) => void;
4533}
4534
4535declare global {
4536  interface Window {
4537    Meticulous?: Meticulous;
4538  }
4539}
4540\`\`\`
4541
4542See [TypeScript Types](${o.TYPESCRIPT_TYPES_URL}) for importable types.
4543
4544---
4545
4546## See Also
4547
4548- [Record Custom Values](${o.RECORD_CUSTOM_VALUES_URL}) - Detailed custom values guide
4549- [Custom Event API](${o.USE_CUSTOM_EVENT_API_URL}) - Advanced event handling
4550- [Handle File Uploads](${o.HANDLE_FILE_UPLOADS_URL}) - File upload patterns
4551`,eh=`---
4552{
4553  "title": "Testing Multiple Apps or App Variants"
4554}
4555---
4556
4557# {% $frontmatter.title %}
4558
4559### I have a single app, but with multiple variants deployed under different configurations to different URLs. How do I use Meticulous to test them?
4560
4561When 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
4562normally 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}).
4563
4564You'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.
4565
4566If 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.
4567
4568### I have multiple independent apps in the same monorepo. How do I use Meticulous to test them?
4569
4570You'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
4571to set up each app to record sessions in a different Meticulous project.
4572
4573If you're using Github Actions to run Meticulous in CI you can then set up a separate call
4574to \`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.
4575
4576If you're using Vercel, or another service that provides preview URLs, then you'll want to configure Vercel deployments for each application separately.
4577Navigate to the project settings page for each Meticulous project and configure that project to test against only the relevant Vercel deployments by
4578selecting the appropriate environments in the \`Environments to Test Against\` section.
4579
4580### GitHub check names with multiple projects
4581
4582When 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)'*.
4583
4584If 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.
4585
4586Reach out to [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll help you get set up.
4587`,ep=`---
4588{
4589  "title": "Testing Feature Flags with Meticulous"
4590}
4591---
4592
4593# {% $frontmatter.title %}
4594
4595> **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.
4596
4597By default Meticulous will snapshot the network responses, local storage values, cookies and session storage values when a session is
4598originally recorded, and [stub out and replay those values](${o.NETWORK_STUBBING_EXPLANATION_URL}) when later replaying the session. This allows
4599Meticulous to test across varied feature flag configurations.
4600
4601If two recorded sessions executed different code because a flag was on in one and off in the other, Meticulous's
4602[coverage-based test suite](${o.TESTING_POOL_URL}) will often keep both. Replays stub the recorded network and storage, so those sessions keep
4603the flag values they were recorded with. Meticulous does not enumerate flag combinations as its own selection signal.
4604
4605For 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
4606replays session A it will replay with *my_new_feature* disabled, and when it replays session B it will replay with *my_new_feature* enabled.
4607If those sessions cover different code paths, both are likely to stay in the suite.
4608
4609### Letting Meticulous Override Named Feature Flags
4610
4611Meticulous can ask your application to use a specific value for a *named* feature flag during a replay,
4612so that a replay can exercise code behind a flag that was off — or did not exist — when the session was
4613recorded.
4614
4615To opt in, resolve the value once: prefer \`getFlagOverride\`, otherwise use your own evaluation, then
4616**record that same value** with \`recordFeatureFlag\` before returning it. The two APIs are complementary
4617— \`getFlagOverride\` asks Meticulous which value to use, and \`recordFeatureFlag\` tells Meticulous which
4618value your app actually used. Recording a snapshot from \`getAllFlags()\` (or similar) will miss a forced
4619value, because the SDK never saw the override.
4620
4621\`\`\`typescript
4622const checkGate = (gateName: string) => {
4623  const override = window.Meticulous?.context?.getFlagOverride?.(gateName);
4624  const value = override?.overridden
4625    ? Boolean(override.value)
4626    : myStatsigClient.checkGate(gateName);
4627  window.Meticulous?.context?.recordFeatureFlag?.(gateName, value);
4628  return value;
4629}
4630\`\`\`
4631
4632The same few lines work for any on/off gate, including one you've written yourself. For example,
4633if your app resolves flags from the query string:
4634
4635\`\`\`typescript
4636const resolveFlag = (flagKey: string) => {
4637  const override = window.Meticulous?.context?.getFlagOverride?.(flagKey);
4638  const value = override?.overridden
4639    ? Boolean(override.value)
4640    : flagsFromQueryString[flagKey] || false;
4641  window.Meticulous?.context?.recordFeatureFlag?.(flagKey, value);
4642  return value;
4643}
4644\`\`\`
4645
4646How you consume \`override.value\` depends on the helper you wrap — \`Boolean()\` is not always right.
4647Always record the flag's resolved value (the thing that describes the cohort), not a comparison boolean:
4648
4649- **On/off gate** (\`checkGate(name)\` returns a boolean): \`Boolean(override.value)\`, as above. Record that boolean.
4650- **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.
4651- **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:
4652
4653\`\`\`typescript
4654const isTreatment = (name: string, expected: string | boolean) => {
4655  const override = window.Meticulous?.context?.getFlagOverride?.(name);
4656  if (override?.overridden) {
4657    window.Meticulous?.context?.recordFeatureFlag?.(name, override.value);
4658    return override.value === expected;
4659  }
4660  return originalIsTreatment(name, expected);
4661}
4662\`\`\`
4663
4664If you wrap the value-read underneath an equality-check (\`getExperimentValue\` under \`editorExperiment\`),
4665use the value-read rule and record there — the \`===\` already happens above you.
4666
4667Hook the helper your application actually calls. \`getFlagOverride\` returns \`{ overridden: false }\` whenever
4668Meticulous has no override for that flag, and always does so for real users being recorded, so it is safe
4669to leave in production code. Use optional chaining as shown above so your application also works when the
4670Meticulous snippet isn't loaded.
4671
4672It also composes with the blanket default described below: check for an override first, then fall back to
4673defaulting unrecognised flags to enabled, then record the value you return.
4674
4675### Improving Test Coverage with New Feature Flags
4676
4677As mentioned above, sessions recorded with different flag values will keep those values on replay, and
4678coverage-based selection will often keep both. Sessions recorded *prior* to a feature flag being introduced
4679will likely not test the new feature, because they replay old saved network responses and local storage
4680values without an entry for it. Test coverage of new features gated behind flags can therefore stay limited
4681until new sessions are recorded with that flag enabled — unless you opt into \`getFlagOverride\` above, or
4682the default-enabled fallback below.
4683
4684You can enable unrecognised flags by default when running as part of a Meticulous test (i.e. when Meticulous
4685is replaying old network responses or local storage values from before the flag was introduced). We've
4686included instructions for Statsig below, but a similar approach can be applied for any feature flagging
4687framework. **We recommend making this change if you often develop new features gated behind feature flags.**
4688
4689### Configuring Meticulous with Statsig
4690
4691Before checking a feature flag value first check [window.Meticulous?.isRunningAsTest](${o.METICULOUS_WINDOW_OBJECT_URL}), and if so then
4692check if configuration for the feature flag is missing entirely - if it is then default the feature flag to enabled. This then allows
4693Meticulous to use old sessions to test new features. Here's an example for Statsig, however similar approaches can be followed for other
4694feature flagging frameworks, and for Statsig's React integration:
4695
4696\`\`\`typescript
4697import { StatsigClient } from '@statsig/js-client';
4698
4699const myStatsigClient = new StatsigClient(
4700  YOUR_CLIENT_KEY,
4701  { userID: 'a-user' },
4702  ...
4703);
4704await myStatsigClient.initializeAsync();
4705
4706// We wrap checkGate, and use the wrapped version in our code instead of directly calling myStatsigClient.checkGate
4707const checkGate = (gateName: string) => {
4708  const override = window.Meticulous?.context?.getFlagOverride?.(gateName);
4709  if (override?.overridden) {
4710    const value = Boolean(override.value);
4711    window.Meticulous?.context?.recordFeatureFlag?.(gateName, value);
4712    return value;
4713  }
4714
4715  // If the application is running as part of a Meticulous test, and Meticulous is replaying old network responses or local storage values
4716  // 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
4717  // new features.
4718  //
4719  // See https://docs.statsig.com/sdk/debugging
4720  const value =
4721    window.Meticulous?.isRunningAsTest
4722      && myStatsigClient.getFeatureGate(gateName).details.reason.endsWith("Unrecognized")
4723      ? true
4724      : myStatsigClient.checkGate(gateName);
4725  window.Meticulous?.context?.recordFeatureFlag?.(gateName, value);
4726  return value;
4727}
4728\`\`\`
4729
4730### Configuring Meticulous with LaunchDarkly
4731
4732When accessing a variation default it to true if [window.Meticulous?.isRunningAsTest](${o.METICULOUS_WINDOW_OBJECT_URL}) is true, after
4733checking for a named override. Record the value you return:
4734
4735\`\`\`typescript
4736const variation = (flagKey: string, defaultValue: boolean) => {
4737  const override = window.Meticulous?.context?.getFlagOverride?.(flagKey);
4738  const value = override?.overridden
4739    ? override.value
4740    : client.variation(
4741        flagKey,
4742        window.Meticulous?.isRunningAsTest ?? defaultValue,
4743      );
4744  window.Meticulous?.context?.recordFeatureFlag?.(flagKey, value);
4745  return value;
4746}
4747\`\`\`
4748
4749We recommend wrapping your client to make this the automatic behavior rather than updating every call site.
4750
4751## Related Pages
4752
4753- [Recording the context of a user session](${o.RECORD_SESSION_CONTEXT_URL}) - Learn how to record feature flags and other session context
4754- [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}) - Get type definitions for the \`window.Meticulous\` object
4755`,em=`---
4756{
4757  "title": "Troubleshoot issues when recording on one environment and simulating on another"
4758}
4759---
4760
4761# {% $frontmatter.title %}
4762
4763You can record sessions from one environment (for example, production, or localhost) and simulate them against another environment
4764(for example, a preview URL). However the sessions may fail to simulate if there are significant differences between the environments, for example:
4765
4766 - **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**.
4767
4768   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.
4769
4770   **Solution**: Use relative URLs for static assets in your HTML:
4771   \`\`\`html
4772   <!-- Instead of this: -->
4773   <script src="https://production.example.com/dist/app.js"></script>
4774
4775   <!-- Use this: -->
4776   <script src="/dist/app.js"></script>
4777   \`\`\`
4778
4779   This ensures assets are loaded from whatever environment the session is being simulated against.
4780
4781 - **Differences in authentication configuration**: If the authentication is configured differently on the different environments, and the frontend javascript performs s
4781ome basic authentication checks before talking to the backend then sessions may not simulate correctly across environments.
4782
4783   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.
4784
4785 - **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.
4786
4787If you're using Vercel, Netlify, or similar preview URLs, then Meticulous will simulate sessions against the preview URL of the base commit
4788on 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
4789also important that the preview URLs of the main branch and the pull request branches are configured to run the app with the same configuration.
4790In this case please check the environment variables and configuration you use to run & build your app are the same for the deployments of the
4791main branch (production deploys) and the deployments of pull request branches (preview deploys). If the configuration is not aligned then it's
4792possible that you could get [false positive screenshot diffs](${o.FIX_FALSE_POSITIVES_URL}).
4793
4794### How do I solve this?
4795
4796To fix these issues you may need to unify some of the configurations across the environments you are recording and simulating on. For example,
4797if there are fundamental differences in the URL routing, then you could alias the routes on the different environments so that they match,
4798and sessions can be simulated.
4799
4800You 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
4801test, rather than for an end user, and if so modify the behaviour of the app to work around the differences between the environments. For
4802example, 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,
4803then 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
4804to the backend would fail, but since Meticulous mocks out all responses from the backend anyway it doesn't matter.
4805
4806If you have any questions reach out to [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll be happy to help.
4807`,ef="server-side-rendering",eg="env-differences",ey="pausing-meticulous-replays",ew="other-causes",eb="general-techniques",ev=`---
4808{
4809  "title": "Fix False Positive Diffs"
4810}
4811---
4812
4813# {% $frontmatter.title %}
4814
4815## Overview
4816
4817Ideally 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
4818show up that are unrelated to your code change could be due to:
4819
4820 - [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)
4821 - 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})
4822 - Or, [executing asynchronous tasks that Meticulous doesn't natively handle](#${ey})
4823 - Or, [other causes](#${ew})
4824
4825In all of these cases, if you can't solve the underlying cause, then you can just mark the diff to be ignored:
4826
4827{% anchor id="${eb}" /%}
4828### Configuring certain diffs to be ignored
4829
4830You can configure diffs inside certain elements to be ignored by adding a CSS selector to the
4831_'Elements to ignore when comparing screenshots'_ list in the _'Screenshotting behavior'_ section in project settings, or by adding the
4832\`meticulous-ignore\` class to an element.
4833
4834Please 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
4835test runs for new PRs opened since the original PR adding the \`meticulous-ignore\` class was merged.
4836
4837Alternatively, 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
4838false 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}),
4839and for server side components or server side rendering you can use the [\`meticulous-is-test\` header](#${ef}).
4840
4841{% anchor id="${ef}" /%}
4842## Diffs due to changes in data or the current date when using server side rendering, or rendering NextJS server components
4843
4844By default Meticulous stubs out responses for any fetch or XHR requests from the browser and stubs out the Date functions in the browser.
4845This means that even if the data in your database changes or the time changes you won't see any false positive diffs.
4846
4847However if you're using NextJS server components then Meticulous will re-render those server components on the backend every time it replays
4848a session -- this means that if the data in your database changes or the date changes in the short window of time between when Meticulous
4849replays 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
4850the page inside a server component, then you could see false positive diffs.
4851
4852Meticulous sends a \`meticulous-is-test\` header in every request it makes to your NextJS server. You can use this header to disable parts of
4853your server components which cause flakes in Meticulous tests. It'll always be present (with value '1') if the request is being made as part
4854of a Meticulous test.
4855
4856If you render text based on the current time (for example "Posted 7 minutes ago"), you can configure Meticulous to send a simulated date
4857header with the virtual time by adding a custom header in your project settings (Sett
4857ings > Custom Request Headers). Use the
4858**Simulated Date** template to set the header value - this will be resolved per-request to the virtual time in RFC 7231 format.
4859
4860For example, you could add a custom header named \`meticulous-simulated-date\` using the Simulated Date template, then use it like so:
4861
4862\`\`\`javascript
4863import { headers } from 'next/headers'
4864
4865const getCurrentDate = async () => {
4866  const requestHeaders = await headers()
4867
4868  // If a simulated date header is configured in Meticulous project settings, use it instead of the current date
4869  const simulatedDate = requestHeaders.get('meticulous-simulated-date')
4870  return simulatedDate ? new Date(Date.parse(simulatedDate)) : new Date()
4871}
4872\`\`\`
4873
4874This avoids false positive diffs due to the time changing (e.g. "Posted 7 minutes ago" vs "Posted 8 minutes ago"): Meticulous will send
4875the same timestamp every time for the same request. The timestamp is a UTC date in RFC 7231 format.
4876
4877You can also [configure Meticulous to ignore the diffs using CSS selectors](#${eb}).
4878
4879{% anchor id="${eg}" /%}
4880## Diffs due to differences between environments
4881
4882### Using Vercel
4883
4884If you use Vercel then Meticulous will try to automatically generate previews using the same environmental configuration for both commits to the main branch,
4885and to PR branches. So you shouldn't see any false positive diffs due to differences between environments. If you do, then reach out to
4886the [Meticulous support team](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
4887
4888### Using Netlify, or other preview providers
4889
4890If 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.
4891
4892In 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.
4893
4894For 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)
4895 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
4896 sure that the only screenshot diffs Meticulous shows are due to changes in the code introduced by the pull request being tested, rather than
4897 environmental differences between the environments tested on.
4898
4899To fix this check the environment variables and configuration you use to run & build your app are the same for the deployments of the
4900main branch (production deploys) and the deployments of pull request branches (preview deploys).
4901
4902If it's not possible to unify the configuration across the environments then you can [configure Meticulous to ignore the diffs](#${eb}).
4903
4904### Using GitHub Actions
4905
4906If, 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
4907commit 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
4908that you compile and run your app with the same configuration for both the main branch and the pull request branches.
4909
4910{% anchor id="${ey}" /%}
4911## Diffs due to asynchronous tasks not handled natively by Meticulous
4912
4913Meticulous will automatically wait for most browser tasks to complete before continuing with javascript execution. This ensures the
4914resultant screenshots are deterministic. However, if your application waits for asynchronous events that are *not* handled natively by Meticulous
4915you can use the [Meticulous object on the window](${o.METICULOUS_WINDOW_OBJECT_URL}) to pause the execution of the replay while the
4916asynchronous task is in-progress.
4917
4918For example, let's say you send a message to a custom Chrome extension and then wait for a response.
4919In this case you can tell Meticulous to pause the replay until you have received the expected response:
4920
4921\`\`\`javascript
4922function sendMessageToExtension() {
4923  if (window.Meticulous?.isRunningAsTest) {
4924    // Meticulous will pause test execution for up to 30 seconds. If we don't
4925    // call pause() here Meticulous will sometimes take a screenshot before the
4926    // Chrome extension has responded, and sometimes after, causing flaky tests.
4927    window.Meticulous.replay.pause();
4928  }
4929  chrome.runtime.sendMessage(MY_EXTENSION_ID, "My message", (response) => {
4930    if (window.Meticulous?.isRunningAsTest) {
4931      // Important: we continue the replay even if the request fails
4932      window.Meticulous.replay.resume();
4933    }
4934    if (response.success) {
4935      doSomething(response.data);
4936    }
4937  });
4938}
4939\`\`\`
4940
4941{% anchor id="${ew}" /%}
4942## False positive diffs due to other reasons
4943
4944Meticulous ensures the session simulation executes identically every time, even if there are animations, timers, random number generators,
4945changing data, or changing dates and times. So under normal operation false positive diffs or flakes should not happen.
4946
4947However, if you are making extensive use of web workers, WebGL or WASM, it is possible that in some cases you could see false
4948positive diffs. If you do notice a false positive diff please reach out to the [Meticulous support team](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and
4949we'll take a look into it. You can also [configure Meticulous to ignore the diffs](#${eb}).
4950
4951${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
4952`,ek=`---
4953{
4954  "title": "Recording custom values to use when replaying user sessions"
4955}
4956---
4957
4958# {% $frontmatter.title %}
4959
4960In some situations, you may want to record custom values to use when replaying user sessions. This is typically not needed, but can be
4961useful in some cases — for instance, if:
4962- Meticulous is being served static content when running tests, but your server would normally inject some dynamic values into the page.
4963- 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.
4964
4965### How can I record values?
4966
4967During recording of a user session, you can record custom values using the methods on the \`window.Meticulous.record\` object:
4968- **For object values:** Call \`recordCustomData(key, value)\`. If the value is already present,
4969it will be overwritten.
4970- **For array values:** Call \`pushToCustomDataArray(arrayId, valueToAppend)\`. If the array is not already present, it will be created.
4971The new value will be appended to the array.
4972
4973Note 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
4974it to a string. For instance you could do:
4975\`\`\`javascript
4976window.Meticulous?.record?.recordCustomData(
4977  "preRenderedData",
4978  JSON.stringify(window.PRE_RENDERED_DATA)
4979);
4980\`\`\`
4981For 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).
4982
4983### How can I use the recorded values?
4984
4985When Meticulous is running tests, you can access the recorded values in your code by using the methods on the \`window.Meticulous.replay\`
4986object:
4987- **For object values:** Call \`retrieveCustomData(key)\`. This will return \`null\` if the key is not found.
4988- **For array values:** Call \`retrieveCustomDataArray(arrayId)\`. This will return an empty array if the array is not found.
4989
4990For instance, you could do:
4991\`\`\`javascript
4992const preRenderedData = window.Meticulous?.replay?.retrieveCustomData("preRenderedData");
4993if (preRenderedData) {
4994  window.PRE_RENDERED_DATA = JSON.parse(preRenderedData);
4995}
4996\`\`\`
4997
4998For object values, you can also inject these into request headers in the \`Custom Request Headers\` section of the project settings.
4999
5000## TypeScript Types
5001
5002For TypeScript type definitions for the \`window.Meticulous\` object, see [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}).
5003
5004${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5005`,eS=`---
5006{
5007  "title": "Recording the context of a user session"
5008}
5009---
5010
5011# {% $frontmatter.title %}
5012
5013When recording user sessions with Meticulous, you can add contextual information. This can
5014make sessions easier to find, help your developers debug diffs in these sessions more
5015easily, and let Meticulous optimise session selection across different c
5015ombinations of
5016context (e.g. flags, roles, themes).
5017
5018All \`window.Meticulous?.context.*\` calls below use optional chaining, so they're safe
5019no-ops when the recorder isn't loaded — there's no need to guard them or check
5020\`isRunningAsTest\`.
5021
5022If your project uses TypeScript, install
5023[\`@alwaysmeticulous/sdk-bundles-api\`](${o.TYPESCRIPT_TYPES_URL}) and augment the \`Window\`
5024interface as shown in the [TypeScript Types page](${o.TYPESCRIPT_TYPES_URL}); that gives
5025every call below full type safety. You only need to do this once per project — the same
5026augmentation covers \`recordUserId\`, \`recordUserEmail\`, \`recordFeatureFlag\`,
5027\`getFlagOverride\`, \`recordCustomContext\`, and the rest of \`window.Meticulous\`.
5028
5029## Adding context to user sessions
5030
5031Meticulous provides several methods to record different types of context. If you record
5032the same piece of context multiple times, the last value will be used.
5033
5034### Recording user information
5035
5036You can record the ID and email address of the logged-in user:
5037
5038\`\`\`js
5039// Record the ID of the logged-in user
5040window.Meticulous?.context.recordUserId('user-123');
5041
5042// Record the email address of the logged-in user
5043window.Meticulous?.context.recordUserEmail('[email protected]');
5044\`\`\`
5045
5046This information is associated with the session and makes it easier to find sessions for
5047specific users.
5048
5049A natural place for these calls is wherever you load the current user (e.g. a
5050\`useCurrentUser\` / \`useSession\` hook, or a \`/me\` query). If your app has a separate
5051post-login flow where the user starts logged out and then signs in, you may also want to
5052record there so those sessions pick up the user identity too.
5053
5054### Recording feature flags
5055
5056You can record which feature flags were active during a session (the value should be a
5057string or boolean):
5058
5059\`\`\`js
5060window.Meticulous?.context.recordFeatureFlag('bigUiRefactor', true);
5061window.Meticulous?.context.recordFeatureFlag('checkoutFlowStyle', 'v3');
5062\`\`\`
5063
5064Record the value your app **actually used** after any override. The usual place is the
5065same helper that resolves the flag:
5066
5067\`\`\`js
5068const resolveFlag = (flagKey) => {
5069  const override = window.Meticulous?.context?.getFlagOverride?.(flagKey);
5070  const value = override?.overridden
5071    ? Boolean(override.value)
5072    : flagsFromYourApp[flagKey] || false;
5073  window.Meticulous?.context?.recordFeatureFlag?.(flagKey, value);
5074  return value;
5075};
5076\`\`\`
5077
5078That keeps recording and overriding on one path. \`recordFeatureFlag\` only stores a value;
5079it does not change what a replay sees. \`getFlagOverride\` is what lets Meticulous force a
5080flag so a replay can exercise code that was off when the session was recorded. How you
5081consume \`override.value\` depends on whether the helper is an on/off gate, a value-read,
5082or an equality-check — see
5083[Testing Feature Flags with Meticulous](${o.TESTING_FEATURE_FLAGS}).
5084
5085If you only want to record (and are not wrapping a resolver), you can still loop over the
5086flags your app already evaluates:
5087
5088\`\`\`js
5089// Use whichever flag map your app already has — an SDK snapshot
5090// (e.g. client.getAllFlags() / posthog.getAllFlags()), an API
5091// response from your backend, or a shared flag-provider value.
5092const flags = client.getAllFlags();
5093for (const [name, value] of Object.entries(flags)) {
5094  window.Meticulous?.context.recordFeatureFlag(name, value);
5095}
5096\`\`\`
5097
5098Do **not** rely on that snapshot if you also call \`getFlagOverride\` in the resolver: the
5099SDK will still report the recorded / stubbed value, not the forced one. Record inside the
5100resolver instead.
5101
5102If your app uses **both** a client-side SDK *and* server-evaluated flags (whose resolved
5103values reach the frontend via something like a \`features\` field on \`/me\`), it's worth
5104looping over both — they each affect what the UI renders. Recording the same flag twice
5105is fine; the last value wins.
5106
5107A reasonable place to call this is wherever flags first become available (the SDK's
5108initial-fetch callback, or the effect that resolves your flags API response). If your app
5109re-evaluates flags after login or identity changes, recording there as well keeps the
5110context accurate for sessions that started logged out.
5111
5112### Recording custom context
5113
5114For any other contextual information that doesn't fit into the categories above, you can
5115use the custom context method (again with a string or boolean value):
5116
5117\`\`\`js
5118window.Meticulous?.context.recordCustomContext('userRole', 'admin');
5119\`\`\`
5120
5121Anything that changes how the UI looks or behaves between sessions is worth considering,
5122since recording it helps Meticulous tell those differences apart from real diffs. Common
5123examples:
5124
5125- **User role / permissions** — admin vs regular user, role-gated menus and actions.
5126  Often available on the same user object you read for \`recordUserId\`.
5127- **Tenant / organization / workspace ID** — for multi-tenant apps, which tenant is
5128  active.
5129- **Theme / colour scheme** — whatever drives the \`dark\` class on \`<html>\` or your
5130  theme provider.
5131- **Locale / language** — i18n setting from cookies, localStorage, \`useLocale\`,
5132  \`next-intl\`, \`i18next\`, etc.
5133- **Viewport / layout mode** — compact vs comfortable, sidebar collapsed, etc., when
5134  saved per-user.
5135- **Plan / subscription tier** — free vs pro vs enterprise, when it changes the UI.
5136- **Environment or build version** — useful when comparing diffs across deploys.
5137- **A/B test assignments** outside your main flag provider.
5138
5139It's usually enough to record each value once, where it's first read or initialised. If
5140the value can change mid-session (a theme toggle, a locale switcher, a tenant switcher),
5141recording in the change handler too keeps the context accurate.
5142
5143## Related Pages
5144
5145- [Testing Feature Flags with Meticulous](${o.TESTING_FEATURE_FLAGS}) - Learn how Meticulous tests the feature flags you record
5146- [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}) - Get type definitions for the \`window.Meticulous\` object
5147
5148${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5149`,eT=`---
5150{
5151  "title": "Troubleshoot Authentication & Authorisation Issues"
5152}
5153---
5154
5155# {% $frontmatter.title %}
5156
5157Auth issues are some of the most common issues that you might encounter while setting up Meticulous.
5158This class of issues is easily identifiable by sessions recorded on logged-in pages which, when simulated, consistently redirect to
5159log-in pages or 401 screens.
5160
5161By default Meticulous automatically stubs all XHR, Fetch and WebSocket requests to your backend, and therefore does not necessarily need to
5162authenticate to your backend. In addition Meticulous will also automatically record and replay cookies,
5163local storage & session storage, and so will often be able to authenticate automatically.
5164
5165However if you use server side rendering (SSR),
5166React server components, or wish to test your backend then you may need Meticulous to be able
5167to authenticate correctly with your backend. This works out of the box if your cookie expiries are long enough (at least a week) and
5168you'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
5169Meticulous to authenticate correctly with your backend.
5170
5171However even if you are only using Meticulous to test your frontend, there are still some common issues that can arise:
5172
5173### 1. Backend auth checks when serving the document's HTML
5174
5175When 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
5176longer 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
5177[Meticulous to authenticate correctly against your backend](${o.AUTH_ENABLING_FULL_AUTH_URL}).
5178
5179### 2. Different auth setups across environments
5180
5181This can be an issue if all of the following hold:
5182
5183  - The environment you replay sessions against in CI and the environment you record sessions on (e.g. localhost) use different auth setups, and:
5184  - Your _frontend_ code checks the user's authentication status, and:
5185  - This check would fail if the cookies or local storage were from an environment with a different auth setup (for example, your FE code
5186    redirects the user to login unless a cookie with name \`auth.\${environment-name}\` exists).
5187
5188In 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.
5189
5190### 3. You are using Auth0
5191
5192In 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.
5193
5194## Issues / questions?
5195
5196We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
5197
5198Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
5199
5200`,e_=`---
5201{
5202  "title": "Troubleshoot Recorder"
5203}
5204---
5205
5206# {% $frontmatter.title %}
5207
5208## Validating installation
5209
5210Once 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.
5211
5212If the snippet was installed successfully you should be able to view the recorded session in your
5213{% project_link %}Meticulous dashboard {% /project_link %} in the **Sessions** section.
5214
5215
5216## I see a warning about high abandon rate
5217
5218If 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.
5219The Meticulous recorder snippet will automatically abandon if it detects that the load on the network is too large.
5220
5221This is a protection mechanism for production deployments to prevent any performance degradation.
5222
5223It 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.
5224Or alternatively, if using the \`@alwaysmeticulous/recorder-loader\` package you can set \`isProduction: false\` when calling \`tryLoadAndStartRecorder\`.
5225
5226Setting 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.
5227
5228
5229## I've installed the snippet but why do I not see any sessions in my Meticulous dashboard?
5230
5231It is possible that the Meticulous recorder snippet is abandoning due to high load on the network.
5232
5233You 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.
5234If they do, then the snippet is abandoning due to large payloads being sent to Meticulous. See the section above for how to fix this.
5235
5236
5237## Session recordings are still abandoned even after I've added \`data-is-production-environment="false"\` to the script tag
5238
5239Setting \`data-is-production-environment="false"\` will increase the threshold at which the snippet abandons, but it will not prevent it from abandoning completely.
5240
5241If 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.
5242Alternatively, if using the \`@alwaysmeticulous/recorder-loader\` package you can set \`forceRecording: true\` when calling \`tryLoadAndStartRecorder\`.
5243
5244
5245## Issues / questions?
5246
5247We're always happy to help you with any issues you encounter while setting up or anything you might be unsure about.
5248
5249Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
5250
5251## Next Steps
5252
5253* Relax and sit down while Meticulous is collecting session data for you
5254* [Set up tests to run in CI](${o.CI_SETUP_URL})
5255`,eI=`---
5256  {
5257    "title": "Troubleshoot Simulation Accuracy"
5258  }
5259  ---
5260
5261  # {% $frontmatter.title %}
5262
5263  ## I see inaccurate simulation differences on my PR test run
5264
5265  If you see inaccurate simulation differences on your PR test run, it means that the simulation against the new app version could not
5266  reproduce critical user events and page navigations that occurred during the simulation against the old app version. Inaccurate simulations
5267  can be caused by:
5268  1. **A change in your application since the session was recorded.** Changes such as moving a navigational button or modifying the network
5269  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
5270  is likely the cause, and you can safely ignore the inaccurate simulation diffs. If you want to check directly whether the inaccuracies are
5271  due to a change, you can look for any network requests or “click” user events that were successful in the replay timeline before your
5272  change and unsuccessful in the replay timeline after your change.
5273  2. **A difference between the environment the session was recorded on and the environment it is being replayed in**. You can test if this
5274  is the case by replaying the session against the environment it was originally recorded on and the environment it is being replayed in -
5275  see [Troubleshooting failed simulations](${o.TROUBLESHOOTING_FAILED_SIMULATIONS_STEPS_URL}) for more detailed instructions and for advice on how to fix this.
5276  3. **Something else.** You can debug what may be causing it by replaying the session locally, or looking at the replay timeline. In this
5277  case reach out to Meticulous support by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), and we'll help to get
5278  Meticulous working for your application.
5279
5280  ## I see a warning about low simulation accuracy on my project dashboard
5281
5282  If you see a warning about low simulation accuracy, it means that less than 40% of recent simulations can reproduce critical user events
5283  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
5283overage
5284  for your app until the simulation accuracy improves. See [Troubleshooting failed simulations](${o.TROUBLESHOOTING_FAILED_SIMULATIONS_URL})
5285  for more detailed instructions and for advice on how to fix this.
5286
5287  ## Issues / questions?
5288
5289  We're always happy to help you with any issues you encounter while setting up or anything you might be unsure about.
5290
5291  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}),
5292  and we'll help you get set up.
5293  `,eR=`---
5294  {
5295    "title": "Troubleshoot Failed or Inaccurate Simulations"
5296  }
5297  ---
5298
5299  # {% $frontmatter.title %}
5300
5301  ## Some simulations on my PR test run are failing or inaccurate
5302
5303  Simulations can fail or replay inaccurately for a few reasons:
5304
5305  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
5306  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
5307  it could be due to your application's API changing, triggering errors when Meticulous replays old out-of-date network requests from the
5308  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
5309  flows on your new codebase and select one or more new sessions, with new network responses and user interactions, to correctly cover these paths.
5310  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}).
5311  3. **There are fundamental differences between the environment the session was recorded on and the environment it is being replayed in**, [such
5312  that sessions recorded on one environment can't be replayed on another](${o.RECORD_AND_REPLAY_ON_DIFFERENT_ENVIRONMENTS_URL}).
5313
5314  Follow the steps below to distinguish between these causes and troubleshoot why simulations may be failing.
5315
5316  {% anchor id="${o.TROUBLESHOOTING_FAILED_SIMULATIONS_STEPS_ANCHOR}" /%}
5317  ## Troubleshooting why simulations may be failing
5318
5319  ### 1. Check if the user session can be simulated accurately *against the environment from which it was recorded*
5320
5321  To verify whether the user session can be simulated accurately in the same environment in which it was recorded, you can:
5322
5323  1. Find a session from the **All Sessions** or **Selected Sessions** tab of your {% project_link %}Meticulous dashboard{% /project_link %}.
5324  2. Follow the command to "Replay this session" on the **Simulate** tab of the session page.
5325  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
5326  need to serve your app on that same port locally to replay the session (or edit the \`--appUrl\`).
5327
5328  While simulating the session, keep an eye out for common symptoms of an unsuccessfully simulated session:
5329  - The simulation hits an error screen
5330  - The simulation fails to populate pages with data
5331  - The simulation hits an error state while navigating through a form and stays there for the duration of the simulation
5332
5333  If you see any of these symptoms, and the application has changed somewhat since the session was first recorded, then the
5334  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.
5335  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
5336  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
5337  more information on how Meticulous selects sessions, see [here](${o.TESTING_POOL_URL}).
5338
5339  If the simulation was successful, proceed to the next troubleshooting step.
5340
5341  ### 2. Check if the user session can be simulated accurately *against the environment used for test runs*
5342
5343  To verify whether the user session can be simulated accurately in the environment used for test runs, you can:
5344
5345  1. Navigate back to the session you selected in the previous step.
5346  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'
5347  URL will show a Meticulous secure tunnel URL (\`*.tunnels.meticulous.ai\`) or a Vercel, Netlify or Cloudflare preview URL.
5348  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.
5349
5350  If you're triggering Meticulous tests in CI against Vercel, Netlify or Cloudflare preview URLs then you can run this command as is:
5351  the \`--appUrl\` it is set to run against will most likely still be a valid preview URL.
5352
5353  If however you're triggering Meticulous tests in
5354  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
5355  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
5356  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
5357  your application setup, and you can use this as the \`--appUrl\` to replay the simulation against. It will also post a username and password
5358  which you must make available as \`METICULOUS_TUNNEL_USERNAME\` and \`METICULOUS_TUNNEL_PASSWORD\` environment variables when running the Meticulous
5359  CLI.
5360
5361  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
5362  environment then the issue is likely environmental differences between the recording and test run environments. For more
5363  information on how to debug environmental differences, see [here](${o.FIX_FALSE_POSITIVES_URL}).
5364
5365  If the simulation was successful, please get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}), or
5366  [book a call with us](${p.METICULOUS_SETUP_CALENDLY_LINK}), and we will help you debug this issue.
5367
5368  ### 3. View the simulation timeline to see why it failed to replay
5369
5370  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
5371  pan by dragging. You'll be able to see each screenshot taken, each click, each console log recorded, and each network request made. Failures
5372  are highlighted in red.
5373
5374  Alternatively, simulating a session locally via the **${i.SIMULATION_TAB_NAMES.DEBUG_LOCALLY}** tab with the flags \`--debugger --devTools\`
5375  will let you step through the simulation one user event at a time and pinpoint exactly where the issue is occurring.
5376  `,eC=`---
5377{
5378  "title": "Ensuring the recorder captures early network requests"
5379}
5380---
5381
5382# {% $frontmatter.title %}
5383
5384The Meticulous recorder captures all fetch, XHR and web socket network requests and responses. These same responses are then used when Meticulous later
5385simulates 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
5386\`window.fetch\` and \`XMLHttpRequest\` before any other scripts execute, since these scripts may snapshot references to the original version
5387of the objects.
5388
5389{% anchor id="how-to-make-recorder-first-script" /%}
5390### How do I ensure the Meticulous recorder script is the first script to execute?
5391
5392Please see the instructions [here](${o.INSTALL_RECORDER_URL}) for installing the recorder on your particular framework.
5393
5394**If you're loading the recorder via an NPM dependency:**
5395
5396If you're using
5397\`@alwaysmeticulous/recorder-loader\` then you'll need to wait for the promise returned by \`tryLoadAndStartRecorder\` to
5398resolve before you initialise your app, trigger any network requests, or load any libraries that might take a reference to \`window.fetch\` or
5399\`XMLHttpRequest\`.
5400
5401If you are already waiting for the promise returned by \`tryLoadAndStartRecorder\` to resolve before making any network
5402requests then it may be the case that a library you depend upon is snapshotting a reference to the native version of \`window.fetch\`
5403or \`XMLHttpRequest\` (rather than the version with the Meticulous interceptors) at import time before the recorder script initializes, and then later using
5404this 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.
5405
5406**If you're loading the recorder via a script tag:**
5407
5408If you're adding the Meticulous recorder as a script tag, then you'll need to make sure:
5409
54101. That it is added to your \`index.html\` file, *before* any other script tags.
54112. 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.
54123. That it is present in the initial HTML returned from the server -- you cannot add the script tag dynamically using JavaScript, since if
5413you 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
5414in certain environments then this must be done either server-side, or at build time by templating your HTML.
5415
5416${B}
5417
5418{% anchor id="auto-detect-installation-problems" /%}
5419### How can I detect if I installed it correctly?
5420
5421The 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
5422request to ${N.SNIPPET_URL} prior to any other network activity (as a note, the Meticulous snippet makes a
5423call to Sentry when it initializes, so you might see that early on in the network tab as well).
5424
5425Meticulous can also sometimes automatically detect if the script is not installed correctly. The script monitors for \`window.performance\` entries to
5426detect network requests that occurred before the recorder script initialized. If it detects any missed network requests when recording a session
5427a warning will be shown on the page for that session. However, this only detects cases where the network requests occur before the
5428recorder script initializes, and doesn't detect cases where another script or library which initializes before the recorder script stores a
5429reference to the native \`window.fetch\` or \`XMLHttpRequest\` and then _later_ uses that stored reference to make a network request,
5430without Meticulous being able to intercept it.
5431
5432### Why does Meticulous record and replay network requests?
5433
5434Recording and replaying network requests and responses has a couple of key advantages:
5435
5436  (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
5437  you don't need to set up separate test user accounts.
5438
5439  (2) Meticulous can replay the same responses every time, so that the tests are deterministic and don't depend on the state of your
5440  backend. This allows Meticulous to safely compare the results of the test runs from before your code change and after your code change,
5441  while ensuring that the only differences spotted come from your change to the code: your tests are automatically idempotent. As your BE
5442  APIs change Meticulous will automatically swap out old tests for new ones, keeping them up to date.
5443
5444${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5445`,ex=`---
5446{
5447  "title": "Viewing source coverage information in Meticulous"
5448}
5449---
5450
5451# {% $frontmatter.title %}
5452
5453Meticulous 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
5454to your main branch to avoid delaying PR runs).
5455
5456## How can I view my source coverage?
5457
5458To view your coverage information, visit your project's landing page on Meticulous (that is, the \`Overview\` tab). From there click the
5459\`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
5460you'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
5461covered and not covered.
5462
5463## How can I serve source maps so Meticulous finds them?
5464
5465Meticulous will autodetect your source maps in three different ways (you only need to do one of these):
5466
54671. 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\`
5468extension added at the end. For instance, if you have a file \`https://mysite.com/static/assets/index.js\` then you should serve the
5469source map at \`https://mysite.com/static/assets/index.js.map\`.
54702. You have a \`sourceMappingURL\` comment at the end of your file that points to the source map as documented
5471[here](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html).
54723. You set the \`SourceMap\` HTTP header on the response that serves the file to point to the source map as documented
5473[here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap).
5474
5475
5476## CSS source maps
5477
5478Meticulous also reports which of your stylesheets each test covered. Doing that needs a source map for the bundled CSS your app
5479loaded, which is a separate artifact from your JavaScript source maps — turning source maps on for JavaScript does not
5480necessarily produce one. Meticulous finds a CSS source map in the same three ways listed above: a \`.map\` file served next to the
5481CSS asset, a \`sourceMappingURL\` comment at the end of the stylesheet, or a \`SourceMap\` response header.
5482
5483### Vite
5484
5485Vite does not emit CSS source maps for production builds at all
5486([vitejs/vite#2830](https://github.com/vitejs/vite/issues/2830)), and \`build.sourcemap: true\` covers JavaScript only — it does
5487nothing for CSS. Without a CSS source map a bundled stylesheet is opaque to us, and its coverage cannot be attributed to any file
5488in your repository.
5489
5490Our plugin closes that gap. It reconstructs a \`<asset>.css.map\` for every CSS asset your build emits, and appends the
5491\`sourceMappingURL\` comment that points at it:
5492
5493\`\`\`shell
5494npm install @alwaysmeticulous/recorder-plugin@latest --save-dev
5495\`\`\`
5496
5497\`\`\`typescript
5498// vite.config.ts
5499import CssSourcemapPlugin from "@alwaysmeticulous/recorder-plugin/css-sourcemap";
5500import { defineConfig } from "vite";
5501
5502export default defineConfig({
5503  plugins: [CssSourcemapPlugin()],
5504});
5505\`\`\`
5506
5507The same plugin is available as the named export \`cssSourcemapPlugin\` if you prefer. It is independent of the recorder-injection
5508plugin in the same package, so you can use either or both, and it works under Rolldown — used by both the \`rolldown-vite\`
5509package and Vite 8 — as well as under stock Vite.
5510
5511If your Vite project lives in a subdirectory rather than at the root of your repository, pass \`root\` so that the emitted source
5512paths match the paths Meticulous sees:
5513
5514\`\`\`typescript
5515CssSourcemapPlugin({ root: path.resolve(__dirname, "../..") });
5516\`\`\`
5517
5518#### How precise is the attribution?
5519
5520Every stylesheet is attributed to the file it came from. Line-level accuracy depends on the stylesheet:
5521
55221. Plain CSS in its own file maps line for line.
55232. Sass/SCSS and Less map approximately — Vite drops preprocessor maps in production.
55243. Tailwind \`@import\`s map to the imported file and line for rules the plugin can still find in the compiled CSS. Generated
5525utilities stay on the Tailwind entry, not on the \`className\` that produced them.
55264. A non-Tailwind \`@import\` that Vite inlines still gets the right file, but line numbers below the import 
5526can shift.
5527
5528File-level attribution is enough to see which stylesheet a change touched. Tailwind imported rules also get line-level maps;
5529utilities do not.
5530
5531#### The tradeoff: CSS minification
5532
5533The plugin disables Vite's CSS minification, and that is what makes the emitted maps accurate — Vite minifies a CSS asset after
5534the plugin has recorded where each stylesheet landed inside it. Measured on two real apps, the shipped CSS grew by about 11%
5535gzipped on a React and Mantine app, and about 6% on a Tailwind app, with build time within noise. The \`.css.map\` files themselves
5536are only fetched by tooling and never on a page load, so they add nothing to what your users download.
5537
5538Because of that cost, enable the plugin on the build whose coverage Meticulous collects rather than on every production build.
5539You can keep minification on with \`disableCssMinify: false\`, but then the plugin emits no map at all and warns, rather than
5540emitting a partial one that would attribute some stylesheets' rules to their neighbours.
5541
5542
5543## Monorepos
5544
5545If 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.
5546
5547For example, if your app is within \`apps/frontend\` and you have a package \`packages/utils\` that the app depends on,
5548you&apos;ll need to build source maps for both of these packages. You might need to adjust your build process to load source maps
5549for your dependencies, e.g. by using \`source-map-loader\` in your webpack config.
5550
5551
5552### Example Next.js setup
5553
55541. Enable source maps generation in your dependent packages.
5555  \`packages/utils/tsconfig.json\`:
5556    \`\`\`json
5557    {
5558      ...
5559      "sourceMap": true,
5560      "declarationMap": true
5561    }
5562    \`\`\`
55632. Install \`source-map-loader\` in your Next.js project:
5564      \`\`\`bash
5565      npm install --save-dev source-map-loader <OR>
5566      yarn add --dev source-map-loader <OR>
5567      pnpm add --save-dev source-map-loader
5568      \`\`\`
55693. Add the following to your Next.js project&apos;s \`next.config.js\` in order to load source maps from your monorepo packages:
5570      \`\`\`javascript
5571      module.exports = {
5572        ...
5573        webpack: (config, { isServer }) => {
5574          // Ensure TypeScript source maps from monorepo packages work correctly
5575          config.module.rules.push({
5576            test: /\\.js$/,
5577            use: ['source-map-loader'],
5578            enforce: 'pre',
5579          });
5580
5581          // Silence source map parsing warnings.
5582          config.ignoreWarnings = [
5583            ...(config.ignoreWarnings || []),
5584            /Failed to parse source map/,
5585          ];
5586
5587          return config;
5588        },
5589      };
5590      \`\`\`
5591
5592${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5593`,eA=`---
5594{
5595  "title": "Blocking Requests During Replay"
5596}
5597---
5598
5599# {% $frontmatter.title %}
5600
5601Meticulous 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).
5602
5603You can configure **blocked request patterns** to abort these requests during replay, preventing them from interfering with your tests.
5604
5605## When to use blocked requests
5606
5607Use blocked requests when a third-party service causes flaky diffs or non-deterministic behavior during replay. Common examples include:
5608
5609- **Analytics and tracking scripts** (e.g. Segment, Amplitude, Google Analytics)
5610- **Error monitoring** (e.g. Sentry, Datadog, LogRocket)
5611- **Payment processors** (e.g. Stripe)
5612- **CAPTCHAs** (e.g. reCAPTCHA, hCaptcha)
5613- **Chat widgets** (e.g. Intercom, Zendesk)
5614- **Ad scripts**
5615
5616## Configuring blocked requests
5617
56181. Navigate to your project's **Settings** page.
56192. Scroll to the **Blocked Requests** section.
56203. Click **Add Entry** to add a new pattern.
56214. Fill in one or more of the following fields:
5622   - **Root Domain**: Match requests to a specific domain (e.g. \`stripe.com\`). This matches the domain and all of its subdomains.
5623   - **Resource Type**: Filter by the type of resource being requested (e.g. Script, Document, Image).
5624   - **URL Regex**: A regular expression to match against the full request URL. Use this for more granular control.
56255. Click **Save Changes**.
5626
5627You 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.
5628
5629## How matching works
5630
5631A request is blocked if it matches **any** of the entries in your blocked requests list. Within a single entry, **all** specified fields must match:
5632
5633- **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\`.
5634- **Resource Type**: The request's resource type must exactly match (e.g. \`script\`, \`fetch\`, \`xhr\`).
5635- **URL Regex**: The regular expression must match somewhere in the full request URL. The regex is tested against the complete URL string.
5636
5637
5638${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5639`,eM=`---
5640{
5641  "title": "Configuring Ignore Patterns for Coverage"
5642}
5643---
5644
5645# {% $frontmatter.title %}
5646
5647Meticulous uses a \`.meticulousignore\` file at the root of your repository to exclude files from [source coverage](${o.ENABLE_SOURCE_COVERAGE_
5647URL}) tracking. The file follows the same syntax as [.gitignore](https://git-scm.com/docs/gitignore).
5648
5649## Global ignore patterns
5650
5651Create 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.
5652
5653\`\`\`
5654# .meticulousignore
5655
5656# Exclude generated files
5657src/generated/**
5658
5659# Exclude Storybook
5660apps/storybook/**
5661
5662# Exclude mobile-specific files
5663**/*.ios.*
5664**/*.android.*
5665\`\`\`
5666
5667## Project-specific ignore patterns (monorepos)
5668
5669If 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.
5670
5671Create 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).
5672
5673For example, a monorepo with a \`dashboard\` project and an \`admin\` project might have:
5674
5675\`\`\`
5676# .meticulousignore.dashboard
5677# Applied only when running coverage for the "dashboard" project
5678
5679apps/admin/**
5680\`\`\`
5681
5682\`\`\`
5683# .meticulousignore.admin
5684# Applied only when running coverage for the "admin" project
5685
5686apps/dashboard/**
5687\`\`\`
5688
5689Patterns from \`.meticulousignore\` (global) and \`.meticulousignore.{slug}\` (project-specific) are merged together, so both apply at the same time.
5690
5691## How the slug is computed
5692
5693The slug is derived from your Meticulous project name using these steps:
5694
56951. Convert to lowercase
56962. Replace any character that is not alphanumeric, a hyphen (\`-\`), or an underscore (\`_\`) with a hyphen
56973. Collapse consecutive hyphens into a single hyphen
56984. Strip any leading or trailing hyphens
5699
5700| Project name | Ignore file |
5701|---|---|
5702| \`dashboard\` | \`.meticulousignore.dashboard\` |
5703| \`my-next-cloudflare-app\` | \`.meticulousignore.my-next-cloudflare-app\` |
5704| \`meticulous_app\` | \`.meticulousignore.meticulous_app\` |
5705| \`My Dashboard App\` | \`.meticulousignore.my-dashboard-app\` |
5706| \`apps/admin\` | \`.meticulousignore.apps-admin\` |
5707| \`My App (v2)\` | \`.meticulousignore.my-app-v2\` |
5708
5709Your 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}\`.
5710
5711${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
5712`,eE=`---
5713{
5714  "title": "Handle file uploads"
5715}
5716---
5717
5718# {% $frontmatter.title %}
5719
5720## Overview
5721
5722By default, Meticulous does not automatically store files that users upload during recording. This design decision is intentional:
5723
5724- **Storage efficiency**: Recording file contents would significantly increase storage costs
5725- **Privacy**: Avoiding file storage prevents capturing potentially sensitive user data
5726- **Performance**: File uploads can be large and would slow down session recording
5727- **Practicality**: Most apps only need to test the upload flow, not the file contents themselves
5728
5729When 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.
5730
5731This guide shows you how to handle file uploads in your Meticulous tests using three different approaches.
5732
5733---
5734
5735## Approach 1: Skip Validation (Recommended)
5736
5737**When to use**: Most cases where your app validates that a file is present before proceeding.
5738
5739The 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.
5740
5741### Basic Example
5742
5743\`\`\`typescript
5744const handleClickUpload = () => {
5745  if (window.Meticulous?.isRunningAsTest) {
5746    // Skip validation during Meticulous tests
5747    goToNextStage();
5748  } else if (fileInput.files.length > 0) {
5749    goToNextStage();
5750  } else {
5751    showIsRequiredError();
5752  }
5753}
5754\`\`\`
5755
5756### Single File Input with Validation
5757
5758\`\`\`typescript
5759function ProfilePictureUpload() {
5760  const [file, setFile] = useState<File | null>(null);
5761  const [error, setError] = useState<string>('');
5762
5763  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
5764    const selectedFile = event.target.files?.[0];
5765    if (selectedFile) {
5766      setFile(selectedFile);
5767      setError('');
5768    }
5769  };
5770
5771  const handleSubmit = async () => {
5772    // Skip file validation during Meticulous tests
5773    if (!window.Meticulous?.isRunningAsTest && !file) {
5774      setError('Please select a file');
5775      return;
5776    }
5777
5778    // During tests, formData will be empty, but the mocked backend response
5779    // will return as if the file was successfully uploaded
5780    const formData = new FormData();
5781    if (file) {
5782      formData.append('profilePicture', file);
5783    }
5784
5785    const response = await fetch('/api/upload-profile-picture', {
5786      method: 'POST',
5787      body: formData,
5788    });
5789
5790    const data = await response.json();
5791    // data.imageUrl will be the mocked value during tests
5792    showSuccessMessage(\`Uploaded: \${data.imageUrl}\`);
5793  };
5794
5795  return (
5796    <div>
5797      <input type="file" onChange={handleFileChange} accept="image/*" />
5798      {error && <span className="error">{error}</span>}
5799      <button onClick={handleSubmit}>Upload</button>
5800    </div>
5801  );
5802}
5803\`\`\`
5804
5805### Multiple File Inputs
5806
5807\`\`\`typescript
5808function DocumentUploadForm() {
5809  const [resume, setResume] = useState<File | null>(null);
5810  const [coverLetter, setCoverLetter] = useState<File | null>(null);
5811
5812  const handleSubmit = async () => {
5813    // Validate files only when not running as a test
5814    if (!window.Meticulous?.isRunningAsTest) {
5815      if (!resume) {
5816        alert('Resume is required');
5817        return;
5818      }
5819      if (!coverLetter) {
5820        alert('Cover letter is required');
5821        return;
5822      }
5823    }
5824
5825    const formData = new FormData();
5826    if (resume) formData.append('resume', resume);
5827    if (coverLetter) formData.append('coverLetter', coverLetter);
5828
5829    await fetch('/api/submit-application', {
5830      method: 'POST',
5831      body: formData,
5832    });
5833
5834    // Backend response is mocked during tests, so this will work
5835    navigateToConfirmationPage();
5836  };
5837
5838  return (
5839    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
5840      <div>
5841        <label>Resume (Required)</label>
5842        <input
5843          type="file"
5844          onChange={(e) => setResume(e.target.files?.[0] || null)}
5845          accept=".pdf,.doc,.docx"
5846        />
5847      </div>
5848      <div>
5849        <label>Cover Letter (Required)</label>
5850        <input
5851          type="file"
5852          onChange={(e) => setCoverLetter(e.target.files?.[0] || null)}
5853          accept=".pdf,.doc,.docx"
5854        />
5855      </div>
5856      <button type="submit">Submit Application</button>
5857    </form>
5858  );
5859}
5860\`\`\`
5861
5862### Drag-and-Drop Upload
5863
5864\`\`\`typescript
5865function DragDropUpload() {
5866  const [file, setFile] = useState<File | null>(null);
5867  const [isDragging, setIsDragging] = useState(false);
5868
5869  const handleDrop = (e: React.DragEvent) => {
5870    e.preventDefault();
5871    setIsDragging(false);
5872
5873    const droppedFile = e.dataTransfer.files[0];
5874    if (droppedFile) {
5875      setFile(droppedFile);
5876    }
5877  };
5878
5879  const handleUpload = async () => {
5880    // Skip validation during tests
5881    if (!window.Meticulous?.isRunningAsTest && !file) {
5882      alert('Please drop a file first');
5883      return;
5884    }
5885
5886    const formData = new FormData();
5887    if (file) {
5888      formData.append('file', file);
5889    }
5890
5891    await fetch('/api/upload', { method: 'POST', body: formData });
5892    showSuccessMessage();
5893  };
5894
5895  return (
5896    <div
5897      onDrop={handleDrop}
5898      onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
5899      onDragLeave={() => setIsDragging(false)}
5900      className={isDragging ? 'dragging' : ''}
5901    >
5902      {file ? \`Selected: \${file.name}\` : 'Drop file here'}
5903      <button onClick={handleUpload}>Upload</button>
5904    </div>
5905  );
5906}
5907\`\`\`
5908
5909### Why This Works
5910
5911Since 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:
5912
59131. During recording: Real file uploaded → Real backend response captured → Response includes file metadata/URL
59142. During replay: No file uploaded → Mocked backend response → Same response as recording, so app behaves identically
5915
5916You get complete test coverage of the user flow without needing the actual file contents.
5917
5918---
5919
5920## Approach 2: Store File Contents (For Frontend Processing)
5921
5922**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.
5923
5924Use the [custom values API](${o.RECORD_CUSTOM_VALUES_URL}
5924) to store strings with \`window.Meticulous.record.recordCustomData(key, value)\` and read them during replay with \`window.Meticulous.replay.retrieveCustomData(key)\`.
5925
5926Both 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.
5927
5928{% callout type="warning" title="Replay timing" %}
5929Custom 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.
5930{% /callout %}
5931
5932{% callout type="warning" title="Size limits" %}
5933The 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.
5934{% /callout %}
5935
5936### Storing an Image or Text File
5937
5938Call this helper from your existing file-selection handler. It records the contents after reading the file:
5939
5940\`\`\`typescript
5941async function recordFileContents(file: File) {
5942  const dataUrl = await new Promise<string>((resolve, reject) => {
5943    const reader = new FileReader();
5944    reader.onload = () => resolve(reader.result as string);
5945    reader.onerror = () => reject(reader.error);
5946    reader.readAsDataURL(file);
5947  });
5948
5949  const meticulous = window.Meticulous;
5950  if (meticulous && !meticulous.isRunningAsTest) {
5951    return meticulous.record.recordCustomData('imagePreviewData', dataUrl);
5952  }
5953}
5954\`\`\`
5955
5956At the point where your application needs the saved data during replay:
5957
5958\`\`\`typescript
5959if (window.Meticulous?.isRunningAsTest) {
5960  const dataUrl = window.Meticulous.replay.retrieveCustomData('imagePreviewData');
5961  if (dataUrl !== null) {
5962    processFile(dataUrl);
5963  }
5964}
5965\`\`\`
5966
5967For text such as CSV content, store the text directly instead of a data URL:
5968
5969\`\`\`typescript
5970const meticulous = window.Meticulous;
5971if (meticulous && !meticulous.isRunningAsTest) {
5972  meticulous.record.recordCustomData('csvData', csvText);
5973}
5974
5975if (meticulous?.isRunningAsTest) {
5976  const csvText = meticulous.replay.retrieveCustomData('csvData');
5977  if (csvText !== null) {
5978    processCsv(csvText);
5979  }
5980}
5981\`\`\`
5982
5983### Storing File Metadata with Contents
5984
5985Serialize the data and metadata into one string:
5986
5987\`\`\`typescript
5988const meticulous = window.Meticulous;
5989if (meticulous && !meticulous.isRunningAsTest) {
5990  meticulous.record.recordCustomData('uploadedFile', JSON.stringify({
5991    dataUrl,
5992    fileName: file.name,
5993    fileType: file.type,
5994  }));
5995}
5996
5997if (meticulous?.isRunningAsTest) {
5998  const serializedFile = meticulous.replay.retrieveCustomData('uploadedFile');
5999  if (serializedFile !== null) {
6000    const storedFile = JSON.parse(serializedFile);
6001    processFile(storedFile.dataUrl);
6002  }
6003}
6004\`\`\`
6005
6006---
6007
6008## Approach 3: Custom Event API (Advanced)
6009
6010**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.
6011
6012Use 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.
6013
6014Register 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.
6015
6016\`\`\`typescript
6017function AdvancedFileUpload() {
6018  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6019    if (window.Meticulous?.isRunningAsTest) return;
6020
6021    const file = event.target.files?.[0];
6022    if (!file) return;
6023
6024    const reader = new FileReader();
6025    reader.onload = (e) => {
6026      const payload = {
6027        fileName: file.name,
6028        fileType: file.type,
6029        fileData: e.target?.result as string,
6030      };
6031
6032      const meticulous = window.Meticulous;
6033      if (meticulous && !meticulous.isRunningAsTest) {
6034        const result = meticulous.record.recordCustomEvent(
6035          'PROFILE_IMAGE_READ',
6036          JSON.stringify(payload),
6037        );
6038        if (!result.success) {
6039          console.warn('File contents could not be recorded for replay');
6040        }
6041      }
6042
6043      // Run the same application logic during recording and replay
6044      processUploadedFile(payload);
6045    };
6046    reader.readAsDataURL(file);
6047  };
6048
6049  useEffect(() => {
6050    if (!window.Meticulous?.isRunningAsTest) return;
6051
6052    let active = true;
6053    window.Meticulous.replay.addCustomEventListener(
6054      'PROFILE_IMAGE_READ',
6055      (serializedData) => {
6056        if (active) {
6057          return processUploadedFile(JSON.parse(serializedData));
6058        }
6059      },
6060    );
6061
6062    return () => { active = false; };
6063  }, []);
6064
6065  return <input type="file" onChange={handleFileChange} />
6065;
6066}
6067\`\`\`
6068
6069---
6070
6071## Complete Integration Examples
6072
6073### With FormData and Fetch
6074
6075\`\`\`typescript
6076async function uploadFile(file: File | null) {
6077  // Skip file validation during tests
6078  if (!window.Meticulous?.isRunningAsTest && !file) {
6079    throw new Error('No file selected');
6080  }
6081
6082  const formData = new FormData();
6083  if (file) {
6084    formData.append('file', file);
6085    formData.append('userId', getCurrentUserId());
6086    formData.append('uploadType', 'document');
6087  }
6088
6089  const response = await fetch('/api/v1/upload', {
6090    method: 'POST',
6091    headers: {
6092      'Authorization': \`Bearer \${getAuthToken()}\`,
6093    },
6094    body: formData,
6095  });
6096
6097  if (!response.ok) {
6098    throw new Error('Upload failed');
6099  }
6100
6101  // Backend response is mocked during replay
6102  const result = await response.json();
6103  return result.fileUrl;
6104}
6105\`\`\`
6106
6107### With XMLHttpRequest Progress Tracking
6108
6109\`\`\`typescript
6110function uploadWithProgress(file: File | null, onProgress: (percent: number) => void) {
6111  return new Promise((resolve, reject) => {
6112    // Skip validation during tests
6113    if (!window.Meticulous?.isRunningAsTest && !file) {
6114      reject(new Error('No file selected'));
6115      return;
6116    }
6117
6118    const xhr = new XMLHttpRequest();
6119
6120    xhr.upload.addEventListener('progress', (e) => {
6121      if (e.lengthComputable) {
6122        const percentComplete = (e.loaded / e.total) * 100;
6123        onProgress(percentComplete);
6124      }
6125    });
6126
6127    xhr.addEventListener('load', () => {
6128      if (xhr.status === 200) {
6129        resolve(JSON.parse(xhr.responseText));
6130      } else {
6131        reject(new Error('Upload failed'));
6132      }
6133    });
6134
6135    xhr.addEventListener('error', () => reject(new Error('Network error')));
6136
6137    xhr.open('POST', '/api/upload');
6138
6139    const formData = new FormData();
6140    if (file) {
6141      formData.append('file', file);
6142    }
6143
6144    xhr.send(formData);
6145  });
6146}
6147\`\`\`
6148
6149---
6150
6151## Common Patterns
6152
6153### Multiple Files from Single Input
6154
6155\`\`\`typescript
6156function MultiFileUpload() {
6157  const [files, setFiles] = useState<File[]>([]);
6158
6159  const handleFilesChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6160    const selectedFiles = Array.from(event.target.files || []);
6161    setFiles(selectedFiles);
6162  };
6163
6164  const handleUpload = async () => {
6165    // Skip validation during tests
6166    if (!window.Meticulous?.isRunningAsTest && files.length === 0) {
6167      alert('Please select at least one file');
6168      return;
6169    }
6170
6171    const formData = new FormData();
6172    files.forEach((file, index) => {
6173      formData.append(\`file\${index}\`, file);
6174    });
6175
6176    await fetch('/api/upload-multiple', {
6177      method: 'POST',
6178      body: formData,
6179    });
6180  };
6181
6182  return (
6183    <div>
6184      <input type="file" multiple onChange={handleFilesChange} />
6185      <p>{files.length} files selected</p>
6186      <button onClick={handleUpload}>Upload All</button>
6187    </div>
6188  );
6189}
6190\`\`\`
6191
6192### Conditional File Processing
6193
6194\`\`\`typescript
6195function ConditionalUpload() {
6196  const [shouldValidate, setShouldValidate] = useState(false);
6197
6198  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6199    const file = event.target.files?.[0];
6200    if (!file) return;
6201
6202    if (shouldValidate && !window.Meticulous?.isRunningAsTest) {
6203      // Only process file if validation is enabled and not in test
6204      const reader = new FileReader();
6205      reader.onload = (e) => {
6206        validateFileContents(e.target?.result);
6207      };
6208      reader.readAsText(file);
6209    } else {
6210      // Skip to upload
6211      uploadFile(file);
6212    }
6213  };
6214
6215  return (
6216    <div>
6217      <label>
6218        <input
6219          type="checkbox"
6220          checked={shouldValidate}
6221          onChange={(e) => setShouldValidate(e.target.checked)}
6222        />
6223        Validate file contents before upload
6224      </label>
6225      <input type="file" onChange={handleFileChange} />
6226    </div>
6227  );
6228}
6229\`\`\`
6230
6231---
6232
6233## Error Handling
6234
6235### File Too Large for Custom Values API
6236
6237\`\`\`typescript
6238function SmartFileUpload() {
6239  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6240    const file = event.target.files?.[0];
6241    if (!file) return;
6242
6243    const reader = new FileReader();
6244    reader.onload = (e) => {
6245      const dataUrl = e.target?.result as string;
6246
6247      const meticulous = window.Meticulous;
6248      if (meticulous && !meticulous.isRunningAsTest) {
6249        const result = meticulous.record.recordCustomData('fileData', dataUrl);
6250        if (!result.success) {
6251          console.warn('File contents could not be recorded for replay');
6252          // Handle missing replay data in your application.
6253        }
6254      }
6255
6256      processFile(dataUrl);
6257    };
6258    reader.readAsDataURL(file);
6259  };
6260
6261  return <input type="file" onChange={handleFileChange} />;
6262}
6263\`\`\`
6264
6265### Handling Unsupported File Types
6266
6267\`\`\`typescript
6268function TypeSafeUpload() {
6269  const ALLOWED_TYPES = {
6270    'image/jpeg': ['.jpg', '.jpeg'],
6271    'image/png': ['.png'],
6272    'application/pdf': ['.pdf'],
6273  };
6274
6275  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
6276    const file = event.target.files?.[0];
6277    if (!file) return;
6278
6279    if (!window.Meticulous?.isRunningAsTest) {
6280      if (!Object.keys(ALLOWED_TYPES).includes(file.type)) {
6281        alert(\`Unsupported file type: \${file.type}\`);
6282        event.target.value = ''; // Clear input
6283        return;
6284      }
6285    }
6286
6287    uploadFile(file);
6288  };
6289
6290  const acceptString = Object.values(ALLOWED_TYPES).flat().join(',');
6291
6292  return <input type="file" accept={acceptString} onChange={handleFileChange} />;
6293}
6294\`\`\`
6295
6296---
6297
6298## Summary
6299
6300**Most apps should use Approach 1** (skip validation during tests) because:
6301- Simple and requires minimal code changes
6302- Works with Meticulous' network stubbing
6303- Tests the complete user flow including backend responses
6304- No file size limitations
6305
6306**Use Approach 2** (store file contents) only when:
6307- You need to test frontend file processing logic
6308- The serialized contents fit within the recorder's configured limit
6309- Your app consumes the saved value at a known point during replay
6310
6311**Use Approach 3** (custom event API) for:
6312- Image previews or file processing that must run at the recorded time
6313- Repeated file selections whose individual contents must be preserved
6314- Integration with existing custom event systems
6315
6316For more details on the Meticulous API, see the [window.Meticulous object documentation](${o.METICULOUS_WINDOW_OBJECT_URL}).
6317`,eU=`---
6318{
6319  "title": "Companion Assets (Advanced)"
6320}
6321---
6322
6323# {% $frontmatter.title %}
6324
6325{% callout type="info" title="Companion assets are only relevant to the cloud-compute (tunnel) workflow" %}
6326If 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.
6327{% /callout %}
6328
6329## What are Companion Assets?
6330
6331Companion 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.
6332
6333This is an advanced feature that solves specific performance and deployment challenges.
6334
6335---
6336
6337## When to Use Companion Assets
6338
6339### Next.js Applications
6340
6341**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.
6342
6343**Solution**: Upload the \`.next/static/\` folder as companion assets and configure Meticulous to serve requests to \`/_next/static/\` from this folder.
6344
6345### Large Static Assets
6346
6347**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.
6348
6349**Solution**: Upload these static assets as companion assets so Meticulous serves them directly, bypassing the tunnel.
6350
6351### CDN-Hosted Assets During Recording
6352
6353**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.
6354
6355**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.
6356
6357### Multi-Server Applications
6358
6359**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.
6360
6361**Solution**: Use companion assets to serve assets that would normally come from a separate server.
6362
6363---
6364
6365## How Companion Assets Work
6366
6367When you configure companion assets, Meticulous:
6368
63691. **Uploads** the specified local folder to cloud storage
63702. **Starts** a static file server in the test environment with your assets
63713. **Intercepts** requests matching the regex pattern
63724. **Redirects** matching requests to the local asset server instead of proxying through the tunnel
6373
6374### Request Routing Example
6375
6376Without companion assets:
6377\`\`\`
6378Browser → /_next/static/chunks/main.js → Tunnel → Your App (localhost:3000)
6379\`\`\`
6380
6381With companion assets:
6382\`\`\`
6383Browser → /_next/static/chunks/main.js → Companion Assets Server → Uploaded files
6384\`\`\`
6385
6386---
6387
6388## Configuration
6389
6390Companion assets require **two parameters**, and you must provide **both or neither**:
6391
6392### 1. \`companion-assets-folder\`
6393
6394The path to a local folder containing the static files to upload.
6395
6396- Must be a relative or absolute path on your CI runner
6397- The entire folder contents are uploaded
6398- Folder structure is preserved
6399
6400### 2. \`companion-assets-regex\`
6401
6402A regular expression pattern to match request URLs that should be served from companion assets.
6403
6404- Matches against the request **pathname** (e.g., \`/_next/static/chunks/main.js\`)
6405- Should typically start with \`^\` to match from the beginning
6406- Only GET and HEAD requests are intercepted
6407- Case-sensitive by default
6408
6409{% callout type="warning" title="Both Parameters Required" %}
6410You must provide both \`companion-assets-folder\` and \`companion-assets-regex\`, or neither. Providing only one will result in an error.
6411{% /callout %}
6412
6413---
6414
6415## Complete Examples
6416
6417### Next.js Application
6418
6419Next.js apps are the most common use case for companion assets.
6420
6421#### GitHub Actions Workflow
6422
6423\`\`\`yaml
6424${g}
6425
6426      - name: Setup Node.js
6427        uses: actions/setup-node@v4
6428        with:
6429          node-version: "24"
6430          cache: pnpm
6431
6432      - name: Install dependencies
6433        run: pnpm install --frozen-lockfile
6434
6435      - name: Build Next.js app
6436        run: pnpm build
6437
6438      - name: Prepare companion assets
6439        run: |
6440          # Create companion assets folder
6441          mkdir -p companion-assets/_next
6442          # Copy Next.js static assets
6443          cp -r .next/static companion-assets/_next/
6444
6445      - name: Serve Next.js app
6446        run: |
6447          pnpm start &
6448          sleep 5
6449
6450      - name: Run Meticulous tests
6451        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6452        with:
6453          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6454          app-url: "http://localhost:3000"
6455          companion-assets-folder: "companion-assets"
6456          companion-assets-regex: "^/_next/static/"
6457\`\`\`
6458
6459**Key Points:**
6460- Build the Next.js app first (\`pnpm build\`)
6461- Copy \`.next/static/\` to \`companion-assets/_next/\` (preserves path structure)
6462- Regex \`^/_next/static/\` matches all requests starting with \`/_next/static/\`
6463- Folder structure in \`companion-assets\` matches URL structure
6464
6465#### CLI Usage
6466
6467\`\`\`bash
6468# Build the app
6469pnpm build
6470
6471# Prepare companion assets
6472mkdir -p companion-assets/_next
6473cp -r .next/static companion-assets/_next/
6474
6475# Start the app
6476pnpm start &
6477
6478# Run tests with companion assets
6479npx @alwaysmeticulous/cli ci run-with-tunnel \\
6480  --apiToken="$METICULOUS_API_TOKEN" \\
6481  --appUrl="http://localhost:3000" \\
6482  --companionAssetsFolder="companion-assets" \\
6483  --companionAssetsRegex="^/_next/static/"
6484\`\`\`
6485
6486### Vite App with CDN Assets
6487
6488If your Vite app loads assets from a CDN during production but you want to test with local assets:
6489
6490\`\`\`yaml
6491      - name: Build Vite app
6492        run: pnpm build
6493
6494      - name: Prepare companion assets
6495        run: |
6496          # Copy built assets
6497          mkdir -p companion-assets/assets
6498          cp -r dist/assets/* companion-assets/assets/
6499
6500      - name: Serve app
6501        run: |
6502          pnpm preview &
6503          sleep 3
6504
6505      - name: Run Meticulous tests
6506        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6507        with:
6508          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6509          app-url: "http://localhost:4173"
6510          companion-assets-folder: "companion-assets"
6511          companion-assets-regex: "^/assets/"
6512\`\`\`
6513
6514### Multiple Asset Patterns
6515
6516If you need to serve assets from multiple paths:
6517
6518\`\`\`yaml
6519      - name: Prepare companion assets
6520        run: |
6521          mkdir -p companion-assets
6522          # Copy static assets
6523          cp -r public/static companion-assets/static
6524          # Copy built bundles
6525          cp -r dist/bundles companion-assets/bundles
6526
6527      - name: Run Meticulous tests
6528        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6529        with:
6530          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6531          app-url: "http://localhost:3000"
6532          companion-assets-folder: "companion-assets"
6533          # Match /static/ OR /bundles/
6534          companion-assets-regex: "^/(static|bundles)/"
6535\`\`\`
6536
6537### React App (Create React App)
6538
6539\`\`\`yaml
6540      - name: Build React app
6541        run: pnpm build
6542
6543      - name: Prepare companion assets
6544        run: |
6545          mkdir -p companion-assets
6546          # Copy static files from build output
6547          cp -r build/static companion-assets/static
6548
6549      - name: Serve app
6550        run: |
6551          npx serve -s build -p 3000 &
6552          sleep 3
6553
6554      - name: Run Meticulous tests
6555        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6556        with:
6557          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6558          app-url: "http://localhost:3000"
6559          companion-assets-folder: "companion-assets"
6560          companion-assets-regex: "^/static/"
6561\`\`\`
6562
6563---
6564
6565## Folder Structure Requirements
6566
6567The folder structure inside \`companion-assets-folder\` must match the URL paths.
6568
6569### Example 1: Next.js
6570
6571**Request URL**: \`http://localhost:3000/_next/static/chunks/main.js\`
6572
6573**Regex**: \`^/_next/static/\`
6574
6575**Folder structure**:
6576\`\`\`
6577companion-assets/
6578└── _next/
6579    └── static/
6580        └── chunks/
6581            └── main.js
6582\`\`\`
6583
6584The pathname \`/_next/static/chunks/main.js\` maps to \`companion-assets/_next/static/chunks/main.js\`.
6585
6586### Example 2: Generic Assets
6587
6588**Request URL**: \`http://localhost:3000/assets/images/logo.png\`
6589
6590**Regex**: \`^/assets/\`
6591
6592**Folder structure**:
6593\`\`\`
6594companion-assets/
6595└── assets/
6596    └── images/
6597        └── logo.png
6598\`\`\`
6599
6600### Example 3: Root-Level Assets
6601
6602**Request URL**: \`http://localhost:3000/public/font.woff2\`
6603
6604**Regex**: \`^/public/\`
6605
6606**Folder structure**:
6607\`\`\`
6608companion-assets/
6609└── public/
6610    └── font.woff2
6611\`\`\`
6612
6613---
6614
6615## Regex Pattern Guide
6616
6617### Basic Patterns
6618
6619| Use Case | Regex | Matches |
6620|----------|-------|---------|
6621| Next.js static assets | \`^/_next/static/\` | \`/_next/static/chunks/main.js\`<br/>\`/_next/static/css/app.css\` |
6622| Assets folder | \`^/assets/\` | \`/assets/images/logo.png\`<br/>\`/assets/fonts/font.woff\` |
6623| Static folder | \`^/static/\` | \`/static/js/bundle.js\`<br/>\`/static/css/main.css\` |
6624| Multiple folders | \`^/(assets|static)/\` | \`/assets/logo.png\`<br/>\`/static/main.js\` |
6625| Specific file types | \`^/.*\\.(png|jpg|woff2)$\` | \`/images/logo.png\`<br/>\`/fonts/font.woff2\` |
6626
6627### Advanced Patterns
6628
6629**Match all .js files in /dist/**:
6630\`\`\`
6631^/dist/.*\\.js$
6632\`\`\`
6633
6634**Match versioned assets**:
6635\`\`\`
6636^/assets/v[0-9]+/
6637\`\`\`
6638Matches: \`/assets/v1/main.js\`, \`/assets/v2/app.css\`
6639
6640**Match specific subdirectories**:
6641\`\`\`
6642^/static/(js|css|media)/
6643\`\`\`
6644Matches: \`/static/js/main.js\`, \`/static/css/app.css\`, \`/static/media/logo.png\`
6645
6646---
6647
6648## Troubleshooting
6649
6650### Assets Not Loading
6651
6652**Symptom**: Your app shows missing resources or broken styles during tests.
6653
6654**Possible Causes**:
6655
66561. **Regex doesn't match requests**
6657   - Check the browser network tab in test results
6658   - Verify the exact pathname being requested
6659   - Test your regex pattern at [regex101.com](https://regex101.com)
6660
6661   \`\`\`bash
6662   # Example: If requests are for /_next/static/chunks/main-abc123.js
6663   # This regex won't match (missing trailing slash):
6664   companion-assets-regex: "^/_next/static"
6665
6666   # This will match:
6667   companion-assets-regex: "^/_next/static/"
6668   \`\`\`
6669
66702. **Folder structure doesn't match URL structure**
6671   - Request: \`/_next/static/main.js\`
6672   - Correct: \`companion-assets/_next/static/main.js\`
6673   - Incorrect: \`companion-assets/static/main.js\`
6674
66753. **Files not copied to companion assets folder**
6676   - Verify files exist: \`ls -la companion-assets/_next/static/\`
6677   - Check your build output directory
6678   - Ensure copy commands run after build
6679
6680### Wrong Files Served
6681
6682**Symptom**: Meticulous serves incorrect or outdated files.
6683
6684**Solution**: Ensure you're copying the correct build output.
6685
6686\`\`\`bash
6687# Bad: Copies from source instead of build output
6688cp -r src/assets companion-assets/
6689
6690# Good: Copies from build output
6691cp -r .next/static companion-assets/_next/
6692\`\`\`
6693
6694### Assets Upload Too Large
6695
6696**Symptom**: Companion assets upload times out or fails.
6697
6698**Solutions**:
6699
67001. **Exclude unnecessary files**:
6701   \`\`\`bash
6702   # Only copy necessary files
6703   cp -r .next/static companion-assets/_next/
6704   # Don't copy source maps in production
6705   find companion-assets -name "*.map" -delete
6706   \`\`\`
6707
67082. **Use more specific regex**:
6709   \`\`\`yaml
6710   # Instead of matching all assets
6711   companion-assets-regex: "^/assets/"
6712
6713   # Match only large files (images, fonts)
6714   companion-assets-regex: "^/assets/.*\\.(png|jpg|woff2|woff|ttf)$"
6715   \`\`\`
6716
6717### Debugging Request Matching
6718
6719To see which requests are being intercepted, you can add the \`--printRequests\` flag when using the CLI:
6720
6721\`\`\`bash
6722npx @alwaysmeticulous/cli ci run-with-tunnel \\
6723  --apiToken="$METICULOUS_API_TOKEN" \\
6724  --appUrl="http://localhost:3000" \\
6725  --companionAssetsFolder="companion-assets" \\
6726  --companionAssetsRegex="^/_next/static/" \\
6727  --printRequests
6728\`\`\`
6729
6730For GitHub Actions, you can add the \`meticulous-debug\` label to your PR to keep the secure tunnel open and access detailed logs.
6731
6732---
6733
6734## Performance Considerations
6735
6736### When Companion Assets Help
6737
6738- **Large files**: Images, videos, fonts (>100KB)
6739- **Many files**: Hundreds of small assets loaded per page
6740- **Slow builds**: Next.js apps with large static asset folders
6741- **Bandwidth limits**: CI environments with limited egress
6742
6743### When Companion Assets May Not Help
6744
6745- **Small apps**: Apps with minimal static assets (<10MB total)
6746- **Fast tunnels**: If tunnel performance is already good
6747- **Dynamic assets**: Assets that change based on runtime logic
6748
6749### Measuring Impact
6750
6751Compare test run times with and without companion assets:
6752
67531. Run tests without companion assets
67542. Note the total test run duration
67553. Enable companion assets
67564. Compare the new duration
6757
6758Typical improvements: 10-30% faster test runs for Next.js apps with large static folders.
6759
6760---
6761
6762## CLI Reference
6763
6764### \`ci run-with-tunnel\`
6765
6766\`\`\`bash
6767npx @alwaysmeticulous/cli ci run-with-tunnel \\
6768  --apiToken="<token>" \\
6769  --appUrl="<url>" \\
6770  --companionAssetsFolder="<folder>" \\
6771  --companionAssetsRegex="<regex>"
6772\`\`\`
6773
6774**Parameters**:
6775- \`--companionAssetsFolder\`: Path to local folder (relative or absolute)
6776- \`--companionAssetsRegex\`: Regex pattern (must be properly escaped)
6777
6778### \`ci start-tunnel\`
6779
6780When testing tunnel setup locally:
6781
6782\`\`\`bash
6783npx @alwaysmeticulous/cli ci start-tunnel --port=3000
6784\`\`\`
6785
6786---
6787
6788## GitHub Actions Reference
6789
6790### \`cloud-compute\` Action
6791
6792\`\`\`yaml
6793- uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6794  with:
6795    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6796    app-url: "http://localhost:3000"
6797    companion-assets-folder: "companion-assets"  # Required if regex provided
6798    companion-assets-regex: "^/_next/static/"    # Required if folder provided
6799\`\`\`
6800
6801**Input Types**:
6802- \`companion-assets-folder\`: String (path)
6803- \`companion-assets-regex\`: String (regex pattern)
6804
6805**Defaults**: Both default to empty string (feature disabled)
6806
6807---
6808
6809## Common Patterns
6810
6811### Monorepo with Multiple Next.js Apps
6812
6813\`\`\`yaml
6814      - name: Build apps
6815        run: |
6816          pnpm build:app1
6817          pnpm build:app2
6818
6819      - name: Prepare companion assets for both apps
6820        run: |
6821          mkdir -p companion-assets/app1/_next
6822          mkdir -p companion-assets/app2/_next
6823          cp -r apps/app1/.next/static companion-assets/app1/_next/
6824          cp -r apps/app2/.next/static companion-assets/app2/_next/
6825
6826      - name: Run tests for App 1
6827        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6828        with:
6829          api-token: \${{ secrets.APP1_API_TOKEN }}
6830          app-url: "http://localhost:3000"
6831          companion-assets-folder: "companion-assets/app1"
6832          companion-assets-regex: "^/_next/static/"
6833
6834      - name: Run tests for App 2
6835        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6836        with:
6837          api-token: \${{ secrets.APP2_API_TOKEN }}
6838          app-url: "http://localhost:4000"
6839          companion-assets-folder: "companion-assets/app2"
6840          companion-assets-regex: "^/_next/static/"
6841\`\`\`
6842
6843### Conditional Companion Assets
6844
6845\`\`\`yaml
6846      - name: Prepare companion assets (only for Next.js)
6847        if: \${{ env.FRAMEWORK == 'nextjs' }}
6848        run: |
6849          mkdir -p companion-assets/_next
6850          cp -r .next/static companion-assets/_next/
6851
6852      - name: Run Meticulous tests
6853        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
6854        with:
6855          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
6856          app-url: "http://localhost:3000"
6857          companion-assets-folder: \${{ env.FRAMEWORK == 'nextjs' && 'companion-assets' || '' }}
6858          companion-assets-regex: \${{ env.FRAMEWORK == 'nextjs' && '^/_next/static/' || '' }}
6859\`\`\`
6860
6861---
6862
6863## Summary
6864
6865**Use companion assets when**:
6866- You have a Next.js application
6867- You have large static assets that slow down the tunnel
6868- Assets are served from a CDN during recording but should be local during tests
6869- You need to consolidate assets from multiple servers
6870
6871**Configuration requirements**:
6872- Both \`companion-assets-folder\` and \`companion-assets-regex\` must be provided
6873- Folder structure must match URL path structure
6874- Regex must accurately match the requests you want to intercept
6875
6876**Common mistakes to avoid**:
6877- Mismatched folder structure and URL paths
6878- Regex that doesn't match actual request paths
6879- Forgetting to build the app before copying assets
6880- Copying source files instead of build output
6881`,eL=`---
6882{
6883  "title": "Using the Custom Event API"
6884}
6885---
6886
6887# {% $frontmatter.title %}
6888
6889The Custom Event API allows you to record and replay custom events in your application with deterministic timing.
6890This can be useful if your application relies on external services or web APIs which Meticulous does not mock out by default
6891(e.g. browser extensions, EventSource, etc).
6892
6893- During recording, you can emit custom events with associated data using the \`window.Meticulous?.record?.recordCustomEvent\` method.
6894- During replay, Meticulous will emit custom events at the same time they occurred during the recording.
6895You can listen for these custom events using the \`window.Meticulous?.replay?.addCustomEventListener\` method:
6896
6897## Examples
6898
6899{% anchor id="orientation-example" /%}
6900### Example: Extend Meticulous to support device orientation events
6901
6902\`\`\`typescript
6903const handleOrientationEvent = (opts) => {
6904  // Your existing code that handles the orientation event
6905};
6906
6907if (window.Meticulous?.replay) {
6908  window.Meticulous.replay.addCustomEventListener("deviceOrientationChanged",
6909      (serializedData) => handleOrientationEvent(JSON.parse(serializedData))
6910    );
6911} else {
6912  window.addEventListener(
6913    "deviceorientation",
6914    (event) => {
6915        window.Meticulous?.record?.recordCustomEvent(
6916          "deviceOrientationChanged",
6917          JSON.stringify({ rotation: event.alpha })
6918        );
6919        handleOrientationEvent({ rotation: event.alpha });
6920    }
6921  );
6922}
6923\`\`\`
6924
6925{% anchor id="message-example" /%}
6926### Example: Simulate messages from a 3rd party iframe, without rendering that iframe at test time
6927
6928Imagine 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
6929application correctly handles the user flow around the iframe, including correctly handling any messages received from the iframe.
6930
6931In this case your code may look like this:
6932
6933\`\`\`typescript
6934const iframe = createIFrameAndAddToDOM();
6935iframe.addEventListener("message", handleMessageFromIframe);
6936\`\`\`
6937
6938You can use the custom event API to configure Meticulous to record any messages received from the iframe at recording time, and
6939then simulate those messages at replay time, without actually rendering the iframe at all at replay time:
6940
6941\`\`\`typescript
6942if (window.Meticulous?.replay) {
6943   window.Meticulous.replay.addCustomEventListener(
6944    "message-from-my-iframe",
6945    (serializedData) => handleMessageFromIframe(deserialize(serializedData))
6946  );
6947} else {
6948   const iframe = createIFrameAndAddToDOM();
6949   iframe.addEventListener("message", handleMessageFromIframe);
6950   if (window.Meticulous?.record) {
6951       iframe.addEventListener(
6952        "message",
6953        msg => window.Meticulous.record.recordCustomEvent(
6954          "message-from-my-iframe",
6955          serialize(msg)
6956        )
6957      );
6958   }
6959}
6960\`\`\`
6961
6962## Best Practices
6963
6964- window.Meticulous?.record will only be defined at record time and window.Meticulous?.replay will only be defined at replay time,
6965so you don't need to do a special check to see if you're in recording or replay mode
6966- Use descriptive event names that clearly indicate their purpose
6967- Keep event data simple and serializable
6968- Handle cases where the Meticulous API might not be available (e.g., in development)
6969
6970## TypeScript Types
6971
6972For TypeScript type definitions for the \`window.Meticulous\` object, see [TypeScript Types for window.Meticulous](${o.TYPESCRIPT_TYPES_URL}).
6973`,eP="local-mocks",eO="approval",eN=`---
6974{
6975  "title": "Recorder Developer Tools"
6976}
6977---
6978
6979# {% $frontmatter.title %}
6980
6981The recorder ships with an in-browser developer overlay that you can opt into on
6982any non-production environment. It gives you two things directly inside the page
6983the recorder is running on:
6984
69851. **Manual session selection** — mark the session you are currently recording as
6986   always-run in CI, without leaving your app.
69872. **Local mocks** — switch between mock network scenarios served by a local
6988   [Meticulous Local Mocks](#${eP}) MCP server while you iterate
6989   on UI.
6990
6991It appears as a small dark logo in a corner of the page that you can drag and
6992click to expand. The overlay never loads in production environments and the
6993recorder snippet has to be installed for it to show up — see
6994[Install the Recorder](${o.INSTALL_RECORDER_URL}) if you don't have it set up yet.
6995
6996{% anchor id="enable" /%}
6997## Enabling the developer tools
6998
6999Open the browser DevTools console on any page where the recorder is running and
7000run:
7001
7002\`\`\`javascript
7003window.Meticulous.enableDeveloperTools();
7004\`\`\`
7005
7006This sets a local flag and reloads the page. From then on, every time the
7007recorder loads on any non-production origin in this browser, the developer
7008overlay is loaded too.
7009
7010The overlay does **not** load if the recorder script reports the page as a
7011production environment (\`data-is-production-environment="true"\` on the
7012recorder snippet tag). This is intentional — the manual selection and local
7013mocks features are development tooling, not user-facing features.
7014
7015{% anchor id="manual-selection" /%}
7016## Manually selecting a session
7017
7018Meticulous's [automatic session selection](${o.TESTING_POOL_URL}) is the
7019recommended default for almost everyone: it continuously adapts the suite of
7020sessions executed in CI to maximize coverage of your application as it changes.
7021
7022There are still cases where you want to nail a specific user flow to the
7023suite — for example, a regression you just hand-recorded, or a flow that
7024exercises an edge case the auto-selector keeps missing. The "Manually select
7025session" button in the overlay does exactly that, without you having to leave
7026the page or open the Meticulous app.
7027
7028### How it works
7029
70301. Perform the user flow you want to capture.
70312. Click the Meticulous logo in the corner to open the overlay.
70323. Click **Manually select session**. Optionally give the session a memorable
7033   name (e.g. "Checkout — happy path") and confirm.
70344. A popup will open prompting you to approve the request in the Meticulous
7035   app — see [Approval flow](#${eO}) below.
70365. Once approved, the button switches to **Manually selected**. Click it again
7037   to remove the session from the manually-selected set.
7038
7039A small "Manage manually selected sessions" link in the overlay deep-links you
7040to the project's full manual-selection list in the Meticulous app at any time.
7041
7042### Limits
7043
7044Each project has a hard cap on the number of manually-selected sessions
7045(currently **50**) to keep CI focused. The overlay will warn you about this
7046before you confirm. Manual selection is intended for the handful of flows you
7047explicitly want to pin — use auto-selection for everything else.
7048
7049{% anchor id="${eO}" /%}
7050## The approval flow
7051
7052Because the recorder runs on your own origin (e.g. your local dev URL), rather than meticulous.ai,
7053it cannot directly speak to the Meticulous app with your session cookies. So
7054the first time you ask the overlay to mark or unmark a session, it opens a
7055small approval popup that:
7056
7057- Asks you to sign into the Meticulous app (if you aren't already).
7058- Asks you to explicitly approve giving this browser permission to
7059  mark/unmark sessions for the project.
7060
7061After you approve once, the overlay caches a short-lived bearer token in your
7062browser. Subsequent mark/unmark actions complete instantly without re-prompting,
7063until the token expires (~8 hours) or you sign out.
7064
7065The approval popup is opened synchronously inside the click handler so that
7066browser popup blockers honor it. If your browser still blocks it, allow popups
7067for the page and try again.
7068
7069{% anchor id="${eP}" /%}
7070## Local mocks
7071
7072The "Local Mocks" panel in the overlay is a switcher for mock network scenarios
7073served by the **Meticulous Local Mocks MCP server**, an opt-in tool that
7074records the network traffic of your local browsing and lets your coding agent
7075generate scenarios from it.
7076
7077When the MCP server is running and paired with the overlay, you can pick a
7078scenario from the dropdown to have the overlay intercept and replay its
7079recorded responses for the rest of the page session — useful for flicking
7080between edge cases (e.g. empty state, error state, paginated state) while you
7081iterate on the UI, without having to set up a backend to reproduce them.
7082
7083If you don't have the MCP server running, the panel shows an "Enable
7084Auto-Mocks (alpha)" button that walks you through the one-time pairing step.
7085
7086## Repositioning the overlay
7087
7088Drag the logo to any corner of the page. The overlay remembers which corner it
7089was last in (per browser tab) and snaps to it on the next page load. Because it
7090anchors to the nearest viewport corner with CSS, it stays in view even when you
7091resize the window.
7092
7093{% anchor id="disable" /%}
7094## Disabling the developer tools
7095
7096Run the following in the browser DevTools console:
7097
7098\`\`\`javascript
7099localStorage.removeItem("meticulous.developer-tools");
7100location.reload();
7101\`\`\`
7102
7103The overlay will stop loading on subsequent page loads. Your manual-selection
7104approval token and any locally-marked sessions remain on the server; the
7105overlay just won't surface them on this device.
7106`,eD=`---
7107{
7108  "title": "Custom checks"
7109}
7110---
7111
7112# {% $frontmatter.title %}
7113
7114Meticulous 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.
7115
7116**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.
7117
7118The most common use case is catching **performance and resource regressions**. A refactor might double the number of API calls a page makes, a new depen
7118dency might increase the size of your JavaScript bundle, 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.
7119
7120Your check logic runs in your own CI pipeline and is built with the [\`@alwaysmeticulous/custom-checks\`](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.
7121
7122Complete, runnable example checks live in the [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-checks-examples) repository.
7123
7124## How custom checks work
7125
7126Custom checks split into three phases:
7127
71281. **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\`, \`js-bundle-sizes\`, 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})).
71292. **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.
71303. **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.
7131`,ej=`---
7132{
7133  "title": "Writing a custom check"
7134}
7135---
7136
7137# {% $frontmatter.title %}
7138
7139This 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.
7140
7141It is organised in four sections, mirroring what every custom check does:
7142
71431. **Read the snapshot data** captured during replay.
71442. **Compute the result** of the check by comparing base and head.
71453. **Report the result** back to Meticulous.
71464. **Set up CI** so the check runs on every PR.
7147
7148Everything lives in a single \`report.ts\` file that we build up as we go.
7149
7150A complete, runnable version of this check lives in the [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-checks-examples) repository.
7151
7152## What we will build
7153
7154Each 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.
7155
7156## Prerequisites
7157
7158Before following this guide, make sure you have:
7159
7160- A Meticulous project with [CI tests set up](${o.CI_SETUP_URL}) so every PR produces head and base test runs.
7161- Custom checks enabled for your project. Contact the Meticulous team to enable the custom checks.
7162- Your Meticulous API token.
7163
7164## Pick an example test run to develop against
7165
7166Before 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>\`.
7167
7168It 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.
7169
7170## 1. Read the snapshot data
7171
7172### Set up the reporter project
7173
7174A 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
7174hecks\`](https://www.npmjs.com/package/@alwaysmeticulous/custom-checks) SDK, which handles authentication, resolving test runs, downloading data, and posting results.
7175
7176Create 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:
7177
7178\`\`\`json
7179{
7180  "name": "my-custom-checks",
7181  "private": true,
7182  "scripts": {
7183    "report": "ts-node report.ts"
7184  },
7185  "dependencies": {
7186    "@alwaysmeticulous/custom-checks": "^2.296.0"
7187  },
7188  "devDependencies": {
7189    "ts-node": "^10.8.1",
7190    "typescript": "^5.9.3"
7191  }
7192}
7193\`\`\`
7194
7195Use 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.).
7196
7197The [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-checks-examples) repo has this project ready to run if you'd rather skip the boilerplate.
7198
7199### Resolve a test run
7200
7201Before 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.
7202
7203Two SDK functions get us started:
7204
7205- \`createClient(...)\` opens an authenticated connection to Meticulous from your API token. Every other SDK call takes the client it returns.
7206- \`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.
7207
7208Start with all the imports we will need across the script:
7209
7210\`\`\`typescript
7211import {
7212  createClient,
7213  findTestRunForCustomChecks,
7214  findTestRunByCommitForCustomChecks,
7215  getSnapshotsFromTestRun,
7216  reportCustomCheckResults,
7217  type MeticulousClient,
7218  type Snapshot,
7219  type CustomCheckVerdict,
7220  type ReportedCustomCheckResult,
7221} from "@alwaysmeticulous/custom-checks";
7222
7223const testRunId = process.argv[2];
7224
7225const client = createClient({
7226  apiToken: process.env.METICULOUS_API_TOKEN,
7227  appInfo: "my-app/custom-checks",
7228});
7229
7230const { testRun } = await findTestRunForCustomChecks({
7231  client,
7232  testRunId,
7233  // We are only exploring here, not reporting — don't register this run as
7234  // expecting custom checks (explained under "Report the result").
7235  skipRegisteringExpectedCustomChecks: true,
7236});
7237
7238console.log(\`Resolved test run \${testRun.id} (\${testRun.status}): \${testRun.url}\`);
7239\`\`\`
7240
7241Run it against the example test run you picked earlier:
7242
7243\`\`\`shell
7244METICULOUS_API_TOKEN=<token> pnpm run report -- <testRunId>
7245\`\`\`
7246
7247We 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.
7248
7249### Understand snapshots
7250
7251The 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:
7252
7253- \`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.
7254- \`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.
7255- \`type\` — the kind of snapshot (for example \`network-requests\`).
7256- \`stageDuringSession\` — which screenshot in the session timeline the data belongs to.
7257- \`data\` — the payload, whose shape depends on the snapshot type.
7258
7259Some snapshot types are **built in** and need no application code — Meticulous captures them automatically. The built-in types today are \`network-requests\` (every \`fetch\` / XHR a session made), \`js-bundle-sizes\` (the JavaScript bundles it loaded), 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}).
7260
7261This check uses \`network-requests\`. Each such snapshot's \`data\` describes a single request:
7262
7263\`\`\`typescript
7264interface NetworkRequestSnapshotData {
7265  url: string;
7266  method: string;
7267  requestBody?: string;
7268  status: number | null;
7269  // ...plus request/response headers and other metadata
7270}
7271\`\`\`
7272
7273So one session that made 12 requests produces 12 \`network-requests\` snapshots, all sharing that session's \`sessionId\`.
7274
7275### Fetch the snapshots
7276
7277The 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.
7278
7279Add 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:
7280
7281\`\`\`typescript
7282const NETWORK_REQUESTS_SNAPSHOT_TYPE = "network-requests";
7283
7284const computeNetworkRequestsCheck = async (
7285  client: MeticulousClient,
7286  testRunId: string,
7287): Promise<void> => {
7288  const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7289    client,
7290    testRunId,
7291    snapshotTypes: [NETWORK_REQUESTS_SNAPSHOT_TYPE],
7292  });
7293
7294  console.log(
7295    \`Fetched \${baseSnapshots.length} base and \${headSnapshots.length} head snapshots.\`,
7296  );
7297};
7298\`\`\`
7299
7300Then call it after resolving the run:
7301
7302\`\`\`typescript
7303await computeNetworkRequestsCheck(client, testRun.id);
7304\`\`\`
7305
7306Re-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.
7307
7308## 2. Compute the result of the check
7309
7310### Define the capacity model
7311
7312We 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\`:
7313
7314\`\`\`typescript
7315/** Stable id shown in the Meticulous UI. */
7316const CHECK_ID = "network-requests";
7317
7318/** Surface a non-blocking warning once a session exceeds base capacity by this much. */
7319const WARN_PERCENT_INCREASE_THRESHOLD = 10;
7320/** Require reviewer acknowledgement once a session exceeds base capacity by this much. */
7321const REQUIRE_ACK_PERCENT_INCREASE_THRESHOLD = 20;
7322
7323/** Ignore low-traffic sessions where +1 request is noise. */
7324const MIN_REQUESTS_FOR_ALARM = 3;
7325
7326interface EndpointComparison {
7327  label: string;
7328  baseCount: number;
7329  headCount: number;
7330  delta: number;
7331}
7332
7333interface SessionComparison {
7334  sessionId: string;
7335  // Short description of what the user did in the session (e.g. "Added an item
7336  // to the cart"), used to label the session in the report. \`null\` when the
7337  // session has no description.
7338  sessionDescription: string | null;
7339  baseCount: number;
7340  headCount: number;
7341  delta: number;
7342  percentIncrease: number;
7343  endpoints: EndpointComparison[];
7344}
7345\`\`\`
7346
7347A custom check reports one of three verdicts, typed by the SDK as \`CustomCheckVerdict\`:
7348
7349- \`pass\` — no regression; the check is green and no report is surfaced.
7350- \`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.
7351- \`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.
7352
7353Note 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.)
7354
7355That 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.
7356
7357### Filter out noise
7358
7359Not 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.
7360
7361Each 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:
7362
7363\`\`\`typescript
7364interface NetworkRequestData {
7365  url?: string;
7366  method?: string;
7367  requestBody?: string;
7368}
7369
7370const networkRequestData = (snapshot: Snapshot): NetworkRequestData =>
7371  (snapshot.data ?? {}) as NetworkRequestData;
7372
7373/** Hostname substrings to exclude from the comparison. */
7374const IGNORED_REQUEST_HOST_SUBSTRINGS = [
7375  "sentry.io",
7376  "segment.io",
7377  "google-analytics.com",
7378  "googletagmanager.com",
7379  // Add the third-party hosts your app loads during replay.
7380];
7381
7382const isMeaningfulRequest = (snapshot: Snapshot): boolean => {
7383  const { url } = networkRequestData(snapshot);
7384  if (!url) {
7385    return true;
7386  }
7387  try {
7388    const hostname = new URL(url).hostname.toLowerCase();
7389    return !IGNORED_REQUEST_HOST_SUBSTRINGS.some((needle) =>
7390      hostname.includes(needle),
7391    );
7392  } catch {
7393    // Relative URLs (e.g. "/api/graphql") are same-origin app traffic.
7394    return true;
7395  }
7396};
7397\`\`\`
7398
7399Tailor \`IGNORED_REQUEST_HOST_SUBSTRINGS\` to your stack. Same-origin and relative URLs should always count as meaningful.
7400
7401### Count requests per session
7402
7403To explain *which* endpoints regressed, group meaningful requests by a human-readable label, then bucket them into per-session, per-endpoint counts:
7404
7405\`\`\`typescript
7406/** GraphQL calls by operation name; everything else as \`METHOD /path\`. */
7407const describeRequest = (snapshot: Snapshot): string => {
7408  const { url, method } = networkRequestData(snapshot);
7409  if (!url) {
7410    return "(unknown request)";
7411  }
7412  const verb = (method ?? "GET").toUpperCase();
7413  try {
7414    const { pathname } = new URL(url);
7415    return \`\${verb} \${pathname}\`;
7416  } catch {
7417    return \`\${verb} \${url.split("?")[0]}\`;
7418  }
7419};
7420
7421type RequestCountsByEndpoint = Map<string, number>;
7422
7423const countRequestsBySession = (
7424  snapshots: Snapshot[],
7425): Map<string, RequestCountsByEndpoint> => {
7426  const countsBySession = new Map<string, RequestCountsByEndpoint>();
7427  for (const snapshot of snapshots) {
7428    if (snapshot.type !== NETWORK_REQUESTS_SNAPSHOT_TYPE) continue;
7429    if (!isMeaningfulRequest(snapshot)) continue;
7430
7431    let byEndpoint = countsBySession.get(snapshot.sessionId);
7432    if (!byEndpoint) {
7433      byEndpoint = new Map();
7434      countsBySession.set(snapshot.sessionId, byEndpoint);
7435    }
7436    const label = describeRequest(snapshot);
7437    byEndpoint.set(label, (byEndpoint.get(label) ?? 0) + 1);
7438  }
7439  return countsBySession;
7440};
7441\`\`\`
7442
7443### Compare head against base capacity per session
7444
7445Align 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:
7446
7447\`\`\`typescript
7448const sumCounts = (byEndpoint: RequestCountsByEndpoint): number =>
7449  [...byEndpoint.values()].reduce((total, count) => total + count, 0);
7450
7451const compareEndpoints = (
7452  base: RequestCountsByEndpoint,
7453  head: RequestCountsByEndpoint,
7454): EndpointComparison[] => {
7455  const labels = new Set([...base.keys(), ...head.keys()]);
7456  return [...labels].map((label) => {
7457    const baseCount = base.get(label) ?? 0;
7458    const headCount = head.get(label) ?? 0;
7459    return { label, baseCount, headCount, delta: headCount - baseCount };
7460  });
7461};
7462
7463// First non-null \`sessionDescription\` seen per \`sessionId\`, so each session can
7464// be labelled by what the user did rather than by its opaque id.
7465const collectSessionDescriptions = (
7466  ...snapshotLists: Snapshot[][]
7467): Map<string, string | null> => {
7468  const descriptions = new Map<string, string | null>();
7469  for (const snapshots of snapshotLists) {
7470    for (const snapshot of snapshots) {
7471      const existing = descriptions.get(snapshot.sessionId);
7472      if (existing == null) {
7473        descriptions.set(snapshot.sessionId, snapshot.sessionDescription ?? null);
7474      }
7475    }
7476  }
7477  return descriptions;
7478};
7479
7480const compareSessions = (
7481  baseSnapshots: Snapshot[],
7482  headSnapshots: Snapshot[],
7483): SessionComparison[] => {
7484  const baseCounts = countRequestsBySession(baseSnapshots);
7485  const headCounts = countRequestsBySession(headSnapshots);
7486  const descriptions = collectSessionDescriptions(baseSnapshots, headSnapshots);
7487  const comparisons: SessionComparison[] = [];
7488
7489  for (const sessionId of headCounts.keys()) {
7490    // Only compare sessions that also ran on base.
7491    if (!baseCounts.has(sessionId)) continue;
7492
7493    const baseByEndpoint = baseCounts.get(sessionId) ?? new Map();
7494    const headByEndpoint = headCounts.get(sessionId) ?? new Map();
7495    const baseCount = sumCounts(baseByEndpoint);
7496    const headCount = sumCounts(headByEndpoint);
7497
7498    comparisons.push({
7499      sessionId,
7500      sessionDescription: descriptions.get(sessionId) ?? null,
7501      baseCount,
7502      headCount,
7503      delta: headCount - baseCount,
7504      percentIncrease:
7505        baseCount === 0
7506          ? headCount > 0
7507            ? Infinity
7508            : 0
7509          : ((headCount - baseCount) / baseCount) * 100,
7510      endpoints: compareEndpoints(baseByEndpoint, headByEndpoint),
7511    });
7512  }
7513
7514  return comparisons.sort((a, b) => b.delta - a.delta);
7515};
7516\`\`\`
7517
7518### Decide whether capacity was exceeded
7519
7520Every 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:
7521
7522\`\`\`typescript
7523const hasEnoughTraffic = (c: SessionComparison) =>
7524  Math.max(c.baseCount, c.headCount) >= MIN_REQUESTS_FOR_ALARM;
7525
7526const requiresAck = (c: SessionComparison) =>
7527  hasEnoughTraffic(c) &&
7528  c.baseCount > 0 &&
7529  c.headCount * 100 >=
7530    c.baseCount * (100 + REQUIRE_ACK_PERCENT_INCREASE_THRESHOLD);
7531
7532const isWarning = (c: SessionComparison) => {
7533  if (!hasEnoughTraffic(c) || c.delta <= 0 || requiresAck(c)) return false;
7534  if (c.baseCount === 0) return true; // new meaningful traffic on head
7535  return (
7536    c.headCount * 100 >=
7537    c.baseCount * (100 + WARN_PERCENT_INCREASE_THRESHOLD)
7538  );
7539};
7540
7541const computeVerdict = (
7542  comparisons: SessionComparison[],
7543): CustomCheckVerdict => {
7544  if (comparisons.some(requiresAck)) return "warn-and-require-user-ack";
7545  if (comparisons.some(isWarning)) return "warn-without-requiring-user-ack";
7546  return "pass";
7547};
7548\`\`\`
7549
7550### Build a report and return the result
7551
7552A 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.
7553
7554Because 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:
7555
7556\`\`\`
7557https://app.meticulous.ai/projects/<organization>/<project>/test-runs/<testRunId>/sessions/<sessionId>
7558\`\`\`
7559
7560\`<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.
7561
7562\`\`\`typescript
7563const PROJECT_PATH =
7564  "https://app.meticulous.ai/projects/<organization>/<project>";
7565
7566const sessionUrl = (testRunId: string, sessionId: string): string =>
7567  \`\${PROJECT_PATH}/test-runs/\${testRunId}/sessions/\${sessionId}\`;
7568
7569const buildReport = (
7570  verdict: CustomCheckVerdict,
7571  comparisons: SessionComparison[],
7572  testRunId: string,
7573): string => {
7574  const alarming = comparisons.filter((c) => requiresAck(c) || isWarning(c));
7575  const lines = [
7576    "# Network request capacity",
7577    "",
7578    \`**Verdict:** \${verdict}\`,
7579    "",
7580  ];
7581
7582  alarming.forEach((comparison, index) => {
7583    // Prefer the session's recorded description; fall back to its position when
7584    // the session has none.
7585    const label = comparison.sessionDescription ?? \`session\${index + 1}\`;
7586    lines.push(
7587      \`## [\${label}](\${sessionUrl(testRunId, comparison.sessionId)}) — \${comparison.baseCount} → \${comparison.headCount} requests\`,
7588      "",
7589      "| Endpoint | Base | Head | Δ |",
7590      "| --- | ---: | ---: | ---: |",
7591    );
7592    for (const endpoint of comparison.endpoints.filter((e) => e.delta > 0)) {
7593      lines.push(
7594        \`| \${endpoint.label} | \${endpoint.baseCount} | \${endpoint.headCount} | +\${endpoint.delta} |\`,
7595      );
7596    }
7597    lines.push("");
7598  });
7599
7600  return lines.join("\\n");
7601};
7602\`\`\`
7603
7604A 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.
7605
7606Now 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:
7607
7608\`\`\`typescript
7609const computeNetworkRequestsCheck = async (
7610  client: MeticulousClient,
7611  testRunId: string,
7612): Promise<ReportedCustomCheckResult> => {
7613  const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7614    client,
7615    testRunId,
7616    snapshotTypes: [NETWORK_REQUESTS_SNAPSHOT_TYPE],
7617  });
7618
7619  const comparisons = compareSessions(baseSnapshots, headSnapshots);
7620  const verdict = computeVerdict(comparisons);
7621
7622  return {
7623    checkId: CHECK_ID,
7624    verdict,
7625    summary:
7626      verdict === "pass"
7627        ? "No sessions exceeded their network request capacity"
7628        : \`\${comparisons.filter(requiresAck).length || comparisons.filter(isWarning).length} session(s) exceeded network request capacity\`,
7629    report: {
7630      type: "markdown",
7631      markdown: buildReport(verdict, comparisons, testRunId),
7632    },
7633  };
7634};
7635\`\`\`
7636
7637## 3. Report the result
7638
7639### Run the reporter locally
7640
7641Two more SDK functions complete the round trip:
7642
7643- \`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.
7644- \`reportCustomCheckResults(...)\` posts your computed results back to Meticulous in a single call (more on the "single call" rule below).
7645
7646Wire 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:
7647
7648\`\`\`typescript
7649const dryRun = process.argv.includes("--dryRun");
7650
7651const resolveTestRun = async () => {
7652  const commitShaFlagIndex = process.argv.indexOf("--commitSha");
7653  const commitSha =
7654    commitShaFlagIndex !== -1
7655      ? process.argv[commitShaFlagIndex + 1]
7656      : undefined;
7657
7658  if (commitSha) {
7659    return findTestRunByCommitForCustomChecks({
7660      client,
7661      commitSha,
7662      // On a dry run we are only experimenting, so don't tell the backend that
7663      // results are coming for this run (see below).
7664      skipRegisteringExpectedCustomChecks: dryRun,
7665    });
7666  }
7667
7668  // First positional argument after \`pnpm run report --\`.
7669  const testRunId = process.argv
7670    .slice(2)
7671    .find((arg) => arg !== "--" && !arg.startsWith("-"));
7672  if (testRunId) {
7673    return findTestRunForCustomChecks({
7674      client,
7675      testRunId,
7676      skipRegisteringExpectedCustomChecks: dryRun,
7677    });
7678  }
7679
7680  throw new Error(
7681    "Pass a testRunId argument or --commitSha to identify the test run.",
7682  );
7683};
7684
7685const { testRun } = await resolveTestRun();
7686
7687const checks = [await computeNetworkRequestsCheck(client, testRun.id)];
7688
7689if (dryRun) {
7690  for (const check of checks) {
7691    console.log(\`\${check.checkId}: \${check.verdict}\\n\${check.report.markdown}\`);
7692  }
7693} else {
7694  await reportCustomCheckResults({
7695    client,
7696    testRunId: testRun.id,
7697    results: { status: "complete", checks },
7698  });
7699}
7700\`\`\`
7701
7702Both \`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.
7703
7704Pass \`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.
7705
7706While building the check, keep using your example test run id:
7707
7708\`\`\`shell
7709METICULOUS_API_TOKEN=<token> pnpm run report -- <testRunId> --dryRun
7710\`\`\`
7711
7712In 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.
7713
7714If anything misbehaves, compare against the runnable version in the [\`custom-checks-examples\`](https://github.com/alwaysmeticulous/custom-c
7714hecks-examples) repo.
7715
7716### Report all checks in a single result
7717
7718A 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.
7719
7720You **cannot** report checks separately — for example, posting the network requests result from one CI job and the bundle-size result from another. A test run only accepts one set of results, so a second report is rejected.
7721
7722So 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\`.
7723
7724### Viewing results
7725
7726Open 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.
7727
7728## 4. Set up CI
7729
7730Add 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:
7731
7732\`\`\`yaml
7733- name: Report custom checks
7734  if: always()
7735  working-directory: custom-checks
7736  env:
7737    METICULOUS_API_TOKEN: \${{ secrets.METICULOUS_API_TOKEN }}
7738  run: pnpm run report -- --commitSha \${{ github.sha }}
7739\`\`\`
7740
7741{% callout_card variant="warning" title="Which commit SHA to pass on GitHub" %}
7742\`\${{ 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.
7743{% /callout_card %}
7744
7745Place 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.
7746
7747## Choosing a comparison granularity
7748
7749This 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.
7750
7751Other checks may want a different granularity:
7752
7753- **Across the whole test run.** Aggregate the data over every replay without distinguishing sessions — for example, the total JavaScript bundle size shipped, or the set of unique endpoints called anywhere in the run — and compare the base aggregate against the head aggregate.
7754- **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.
7755
7756Pick whichever granularity makes the regression you care about easiest to detect and explain.
7757`,eF=`---
7758{
7759  "title": "Recording custom snapshots"
7760}
7761---
7762
7763# {% $frontmatter.title %}
7764
7765Built-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(...)\`.
7766
7767Your custom check reporter then downloads those snapshots (alongside built-in ones) with \`getSnapshotsFromTestRun\` and compares base vs head.
7768
7769## Prerequisites
7770
7771- Custom checks enabled for your project. Contact the Meticulous team to enable the custom checks.
7772
7773## The \`recordCustomSnapshot\` API
7774
7775During a replay, call \`window.Meticulous.replay.recordCustomSnapshot\` with:
7776
7777- \`snapshotType\` — a stable name for this kind of snapshot (you will request the same type in your reporter).
7778- \`data\` — a JSON-serializable payload (objects, arrays, strings, numbers, booleans, or \`null\`).
7779- \`versionNumber\` (optional) — increment when you change the shape of \`data\`; Meticulous can surface version mismatches between base and head in the UI.
7780
7781\`\`\`typescript
7782const result = window.Meticulous.replay.recordCustomSnapshot({
7783  snapshotType: "my-metric",
7784  data: { value: 42, unit: "ms" },
7785  versionNumber: 1,
7786});
7787
7788if (!result.success) {
7789  // Custom snapshotting is disabled for this project, or the snapshot was dropped
7790  // (see "When recording is a no-op" below).
7791}
7792\`\`\`
7793
7794### Snapshot type names
7795
7796Choose a \`snapshotType\` that:
7797
7798- Matches \`/^[a-z0-9-]{1,64}$/\` (lowercase letters, digits, and hyphens only).
7799- 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\`, \`js-bundle-sizes\`, \`react-component-renders\`, \`custom-recording\`).
7800
7801Use one type per metric family — for example \`pressure-observer-cpu-read\` for CPU pressure readings, not a new type on every call.
7802
7803### When each snapshot is taken
7804
7805Every 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.
7806
7807Call \`recordCustomSnapshot\` from:
7808
7809- Your app code at any point during replay (for example when an observer fires).
7810- A listener registered with \`addOnBeforeScreenshotListener\` — snapshots are tagged with the screenshot about to be taken.
7811- A listener registered with \`addOnReplayCompletionListener\` — snapshots are tagged with \`final-state\`.
7812
7813Listeners are useful when you want a consistent sampling point (for example, capture a metric before each comparison screenshot):
7814
7815\`\`\`typescript
7816window.Meticulous.replay.addOnBeforeScreenshotListener(({ stageDuringSession }) => {
7817  window.Meticulous.replay.recordCustomSnapshot({
7818    snapshotType: "my-metric",
7819    data: { stageDuringSession, value: measureSomething() },
7820  });
7821});
7822\`\`\`
7823
7824Listeners 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.
7825
7826## When recording is a no-op
7827
7828\`recordCustomSnapshot\` returns \`{ success: false }\` (and does not throw) when:
7829
7830- Custom snapshot recording is **not enabled** for your project.
7831- The page is **not** running as a Meticulous replay (\`window.Meticulous.isRunningAsTest\` is false).
7832
7833Invalid input (bad \`snapshotType\`, non-JSON-serializable \`data\`, or \`undefined\` data) **throws** so you can catch misconfiguration during development.
7834
7835## Example: sampling JS heap memory during replay
7836
7837A 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}).
7838
7839Many 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:
7840
7841\`\`\`typescript
7842const JS_HEAP_MEMORY_SNAPSHOT_TYPE = "js-heap-memory";
7843const SAMPLE_INTERVAL_MS = 1_000;
7844
7845const noop = () => {
7846  /* nothing was started */
7847};
7848
7849const startRecordingJsHeapMemory = (): (() => void) => {
7850  if (typeof window === "undefined") {
7851    return noop;
7852  }
7853
7854  // \`window.Meticulous\` is a discriminated union on \`isRunningAsTest\`; the
7855  // \`replay\` API only exists in the running-as-test variant.
7856  const meticulous = window.Meticulous;
7857  if (!meticulous?.isRunningAsTest) {
7858    return noop;
7859  }
7860  const { replay } = meticulous;
7861
7862  // \`native\` exposes the real (non-stubbed) performance metrics. \`memory\` is
7863  // only present on Chromium.
7864  const { memory } = replay.native.performance;
7865  if (!memory) {
7866    return noop;
7867  }
7868
7869  const record = () => {
7870    replay.recordCustomSnapshot({
7871      snapshotType: JS_HEAP_MEMORY_SNAPSHOT_TYPE,
7872      data: {
7873        usedJSHeapSize: memory.usedJSHeapSize,
7874        totalJSHeapSize: memory.totalJSHeapSize,
7875        time: replay.native.performance.now(),
7876      },
7877      versionNumber: 1,
7878    });
7879  };
7880
7881  // \`native.setInterval\` is the real wall-clock timer captured before stubbing,
7882  // so sampling runs on real time rather than the replay's frozen virtual time.
7883  record(); // initial sample
7884  const interval = replay.native.setInterval(record, SAMPLE_INTERVAL_MS);
7885
7886  return () => replay.native.clearInterval(interval);
7887};
7888\`\`\`
7889
7890Wire \`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\`).
7891
7892## Using recorded snapshots in a custom check
7893
7894In your CI reporter, include your \`snapshotType\` when downloading snapshots for the test run:
7895
7896\`\`\`typescript
7897const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7898  client,
7899  testRunId,
7900  snapshotTypes: ["js-heap-memory", "network-requests"],
7901});
7902\`\`\`
7903
7904Each 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}).
7905`,eH=`---
7906{
7907  "title": "Built-in snapshot types"
7908}
7909---
7910
7911# {% $frontmatter.title %}
7912
7913Meticulous 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.
7914
7915{% callout_card variant="info" title="Contact Meticulous to enable snapshots" %}
7916Built-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.
7917{% /callout_card %}
7918
7919## Snapshot types Meticulous collects for you
7920
7921| Snapshot type | What it captures |
7922| --- | --- |
7923| \`network-requests\` | Every \`fetch\` / XHR issued during replay |
7924| \`js-bundle-sizes\` | JavaScript bundles loaded during replay, broken down by source file |
7925| \`react-component-renders\` | Per-component breakdown of which React components re-rendered and how often, sampled at each screenshot |
7926
7927These are the only built-in **data** snapshot types today. 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}).
7928
7929Do not use these names for your own \`snapshotType\` values when calling \`recordCustomSnapshot\`.
7930
7931## Common snapshot shape
7932
7933Every snapshot — built-in or customer-recorded — is a JSON object with:
7934
7935- \`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 request or bundle load, so you can group data per visual diff stage when debugging a check.
7936- \`data\` — the payload for that entry (schema depends on the snapshot type).
7937- \`versionNumber\` (optional) — only present on customer-recorded snapshots when you pass one to \`recordCu
7937stomSnapshot\`. Built-in snapshots omit this field.
7938
7939Snapshots 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.
7940
7941When you call \`getSnapshotsFromTestRun\`, each returned snapshot also includes:
7942
7943- \`sessionId\` — the session the snapshot was captured in, so you can align the same session on base and head.
7944- \`type\` — the snapshot kind (for example \`network-requests\`), so you can filter by it.
7945- \`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.
7946
7947\`\`\`typescript
7948const { baseSnapshots, headSnapshots } = await getSnapshotsFromTestRun({
7949  client,
7950  testRunId,
7951  snapshotTypes: ["network-requests", "js-bundle-sizes", "react-component-renders"],
7952});
7953\`\`\`
7954
7955## \`network-requests\`
7956
7957**Snapshot type:** \`network-requests\`
7958
7959**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.
7960
7961**What each entry contains:**
7962
7963| Field | Description |
7964| --- | --- |
7965| \`url\` | Request URL |
7966| \`method\` | HTTP method |
7967| \`requestHeaders\` | Request headers (HAR shape) |
7968| \`requestBody\` | Request body, if any |
7969| \`status\` | Status code of the stubbed response served during replay, or \`null\` if the request was not matched to a recorded request |
7970| \`responseHeaders\` | Response headers (HAR shape) |
7971| \`matched\` | \`true\` if the request was matched and stubbed; \`false\` if it was left unmatched |
7972
7973Large 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.
7974
7975**Example entry:**
7976
7977\`\`\`json
7978{
7979  "stageDuringSession": "screenshot-after-event-00003",
7980  "data": {
7981    "url": "https://app.example.com/api/graphql",
7982    "method": "POST",
7983    "requestHeaders": [{ "name": "content-type", "value": "application/json" }],
7984    "requestBody": "{\\"query\\":\\"...\\"}",
7985    "status": 200,
7986    "responseHeaders": [{ "name": "content-type", "value": "application/json" }],
7987    "matched": true
7988  }
7989}
7990\`\`\`
7991
7992
7993## \`js-bundle-sizes\`
7994
7995**Snapshot type:** \`js-bundle-sizes\`
7996
7997**When it is captured:** Every time a JavaScript bundle finishes loading during replay. As well as \`script\` resources, this includes JavaScript loaded via \`<link rel="modulepreload">\` or prefetch — any \`.js\` / \`.mjs\` / \`.cjs\` URL — which the browser does not classify as a script. The same URL loaded twice in one session yields two entries before deduplication (see below).
7998
7999**What each entry contains:**
8000
8001| Field | Description |
8002| --- | --- |
8003| \`url\` | Resolved URL of the bundle |
8004| \`sizeInBytes\` | Decoded (uncompressed) size of the served bundle body, in bytes. \`-1\` when the body could not be retrieved (for example a response served from the browser cache). |
8005| \`status\` | HTTP status code of the served response |
8006| \`sourceBreakdown\` | Optional. Per-source-file breakdown of the bundle's bytes (see below). Omitted when the bundle could not be attributed back to its sources. |
8007
8008Sizes are measured **Node-side** from the served response, not from the browser Performance API. During replay all responses are intercepted, so the browser reports zero transfer sizes for them. The size is always the **decoded body length** — the \`content-length\` header is deliberately ignored so a bundle is sized identically whether it happens to be served compressed or uncompressed, keeping the metric stable across replays.
8009
8010**\`sourceBreakdown\`:** When Meticulous can load a bundle's source map, the entry also carries a \`sourceBreakdown\` — the bundle's generated (minified) bytes attributed back to the original source files they were compiled from. This lets a check link a bundle's size to the source responsible for it, and — because source paths are stable across builds whereas content-hashed bundle URLs are not — makes base-vs-head size diffs meaningful per source file. The list is sorted largest-first and capped to the 100 biggest sources per bundle.
8011
8012**Each \`sourceBreakdown\` entry:**
8013
8014| Field | Description |
8015| --- | --- |
8016| \`script\` | The original source file the bytes came from (for example \`src/components/Foo.tsx\`, or a \`node_modules/...\` dependency). Falls back to the bundle \`url\` for generated runtime/wrapper code that had no source mapping. |
8017| \`bytes\` | Generated (minified) bytes of the bundle attributed to \`script\` via the source map. Summed across the breakdown these approximate the bundle's uncompressed size. |
8018
8019\`sourceBreakdown\` is omitted when source-map attribution was not possible — for example no source map is available for the bundle, or source-map loading is disabled for the project — leaving just \`url\`, \`sizeInBytes\` and \`status\`.
8020
8021**Deduplication:** Within the same \`stageDuringSession\`, Meticulous deduplicates by URL — for example when a chunk is both preloaded and executed in the same stage. When duplicates occur, the **largest** reported \`sizeInBytes\` is kept (a cache-served load may report \`-1\` while the full transfer reports the real size).
8022
8023**Example entry:**
8024
8025\`\`\`json
8026{
8027  "stageDuringSession": "final-state",
8028  "data": {
8029    "url": "https://app.example.com/_next/static/chunks/main-abc123.js",
8030    "sizeInBytes": 184320,
8031    "status": 200,
8032    "sourceBreakdown": [
8033      { "script": "node_modules/react-dom/cjs/react-dom.production.min.js", "bytes": 118500 },
8034      { "script": "src/components/Dashboard.tsx", "bytes": 24310 },
8035      { "script": "https://app.example.com/_next/static/chunks/main-abc123.js", "bytes": 9200 }
8036    ]
8037  }
8038}
8039\`\`\`
8040
8041## \`react-component-renders\`
8042
8043**Snapshot type:** \`react-component-renders\`
8044
8045**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.
8046
8047**What each entry contains:**
8048
8049| Field | Description |
8050| --- | --- |
8051| \`components\` | Per-component cumulative render counts by this stage, most-active first and capped to the busiest components |
8052
8053**Each \`components\` entry:**
8054
8055| Field | Description |
8056| --- | --- |
8057| \`name\` | The component's display name (e.g. \`UserMenu\`), or \`null\` when the name was minified away by your production build |
8058| \`source\` | Original source location of the component as \`<path>:<line>:<col>\` (resolved from your source maps), or \`null\` when it could not be resolved |
8059| \`commits\` | Cumulative number of commits in which this component re-rendered, by this stage |
8060
8061Use \`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.
8062
8063**Example entry:**
8064
8065\`\`\`json
8066{
8067  "stageDuringSession": "screenshot-after-event-00007",
8068  "data": {
8069    "components": [
8070      { "name": "ResultRow", "source": "src/search/ResultRow.tsx:11:0", "commits": 220 },
8071      { "name": "ResultsList", "source": "src/search/ResultsList.tsx:24:0", "commits": 38 },
8072      { "name": null, "source": "node_modules/some-lib/Tooltip.js:8:0", "commits": 9 }
8073    ]
8074  }
8075}
8076\`\`\`
8077`,e$=`---
8078{
8079  "title": "Best practices"
8080}
8081---
8082
8083# {% $frontmatter.title %}
8084
8085Guidance 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}
8085).
8086
8087## Prefer deterministic metrics
8088
8089Start with [built-in snapshot types](${o.CUSTOM_CHECKS_BUILT_IN_SNAPSHOT_TYPES_URL}) such as \`network-requests\`, \`js-bundle-sizes\`, and \`react-component-renders\` — they capture concrete, reproducible signals (request counts, URLs, bundle byte sizes, per-component React re-render counts) that usually only change when your application behaviour or assets change.
8090
8091Metrics 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.
8092
8093## Compare base against head
8094
8095Meticulous 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.
8096
8097Avoid 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").
8098
8099## Filter out sessions with very little data
8100
8101Many 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%).
8102
8103Exclude 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.
8104
8105## When comparing per session, skip head-only sessions
8106
8107If 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.
8108
8109## Reduce noise in what you measure
8110
8111Not 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.
8112
8113In 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.
8114
8115## Require acknowledgement only on a high threshold
8116
8117Reserve 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.
8118
8119## Use non-blocking warnings for highly variable metrics
8120
8121When 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.
8122
8123## Make the report explain the outcome
8124
8125A 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, bundles, or metrics responsible.
8126
8127Where 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.
8128
8129## Report every check in one call
8130
8131All 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.
8132
8133## Test locally before reporting
8134
8135Before 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. 
8135Confirm 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.
8136`,eq=`---
8137{
8138  "title": "Built-in checks"
8139}
8140---
8141
8142# {% $frontmatter.title %}
8143
8144Meticulous can detect non-visual regressions and report them before a pull request is merged.
8145When a check fails, the author of the PR has to acknowledge the result in Meticulous before the pull request can proceed.
8146Every 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.
8147Available built-in checks:
8148
8149- [Accessibility](${o.BUILT_IN_CHECKS_ACCESSIBILITY_URL}): catch new accessibility violations introduced by a PR
8150- [Network requests](${o.BUILT_IN_CHECKS_NETWORK_REQUESTS_URL}): catch PRs that make your app issue meaningfully more network requests
8151- [React component renders](${o.BUILT_IN_CHECKS_REACT_COMPONENT_RENDERS_URL}): catch PRs that make your React components re-render meaningfully more
8152
8153A Meticulous admin can turn these on for your project.
8154`,eB=e=>`
8155## How can ${e} testing be enabled?
8156
8157Reach out to the Meticulous team at [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) and we'll turn it on for your project.
8158The check can optionally be enabled for a subset of users rather than the entire team.
8159`,eG=e=>`
8160## Does the check block merging?
8161
8162The ${e} check is blocking by default, but it can be configured to never block pull requests.
8163You can change this yourself in the project settings, no Meticulous admin needed.
8164`,eW=`---
8165{
8166  "title": "Accessibility"
8167}
8168---
8169
8170# {% $frontmatter.title %}
8171
8172Meticulous supports accessibility regression testing.
8173When a pull request is opened, Meticulous verifies that it does not introduce new accessibility regressions.
8174If it does, the author of the pull request is notified with a comment and a failing CI check.
8175Meticulous reports only **new** regressions, ignoring pre-existing ones.
8176
8177![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)
8178
8179## How does detection work?
8180
8181Every time Meticulous takes a screenshot of your application, it runs an accessibility check on the rendered web page.
8182It then compares the pre-existing defects with the new ones and reports the freshly introduced regressions.
8183Meticulous 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.
8184
8185## Which accessibility rules are tested?
8186
8187Meticulous checks your application against a curated list of accessibility rules.
8188These 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.
8189In 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.
8190AA 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.
8191Applying 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.
8192Each rule's badge shows its level and earliest WCAG version; the rule also applies to later versions.
8193
8194These 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.
8195A newly enabled rule starts reporting new violations once a base run has scanned that rule too.
8196
8197${eG("accessibility")}${eB("accessibility")}`;var ez=e.i(896773);let eV=`---
8198{
8199  "title": "Network requests"
8200}
8201---
8202
8203# {% $frontmatter.title %}
8204
8205Meticulous can check whether a pull request introduces a regression in terms of network traffic.
8206A 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.
8207When network traffic grows past a threshold, Meticulous can block the pull request until the author reviews the per-endpoint breakdown and acknowledges the change.
8208This catches performance regressions that are invisible to visual testing but can increase backend load, infrastru
8208cture costs, and latency for users.
8209
8210![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)
8211
8212## When does the check fail?
8213
8214If Meticulous finds a user flow that issues at least **${ez.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.
8215In case Meticulous finds a user flow that issues at least **${ez.DEFAULT_WARN_PERCENT_INCREASE_THRESHOLD}% more** requests than base, that is reported as an informational warning that leaves the check passing.
8216Both thresholds can be configured in the project settings.
8217
8218## Which requests are counted?
8219
8220Every \`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).
8221Those are excluded so that an extra analytics beacon can never flag your pull request; requests to your own backend always count.
8222The list of ignored hosts can be customised in the project settings.
8223
8224${eG("network requests")}${eB("network traffic")}`,{warnPercentIncreaseThreshold:eK,failPercentIncreaseThreshold:eY}=ez.DEFAULT_REACT_COMPONENT_RENDERS_CHECK_CONFIG,eJ=`---
8225{
8226  "title": "React component renders"
8227}
8228---
8229
8230# {% $frontmatter.title %}
8231
8232Meticulous checks whether a pull request introduces a regression in how many times your React components render.
8233Render regressions rarely show up visually — the page looks identical while a component quietly re-renders hundreds of extra times.
8234This check can block a pull request with this kind of regression from being merged, preventing the author from introducing a frontend performance regression.
8235
8236![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)
8237
8238## When does the check fail?
8239
8240If Meticulous finds a component that renders at least **${eY}% 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.
8241A component that renders at least **${eK}% more** is reported as a warning that leaves the check passing.
8242The thresholds are configurable in the project settings.
8243
8244## Which components are considered?
8245
8246Only first-party components are considered, since third-party components typically provide a weaker signal.
8247
8248${eG("react component renders")}${eB("react component renders")}`,eX=`---
8249{
8250  "title": "Enabling Meticulous to replay sessions fully authenticated"
8251}
8252---
8253
8254# {% $frontmatter.title %}
8255
8256Auth issues are some of the most common issues that you might encounter while setting up Meticulous.
8257This class of issues is easily identifiable by sessions recorded on logged-in pages which, when simulated, consistently redirect to
8258log-in pages or 401 screens.
8259
8260Note 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
8261do wish to enable full authentication, then select the auth provider that you're using from the options below:
8262
8263{% tabs %}
8264{% tab label="Auth0" %}
8265
8266## Auth0
8267
8268[Auth0](https://auth0.com/) is a popular auth provider that is used by many web apps. There are many different integration methods with
8269Auth0, but, at a high level, there are two main patterns:
8270
8271### Is the user session managed in the browser?
8272
8273This integration pattern is most common in single page applications (SPAs). In these methods, the user session is managed in
8274the browser, and the browser is responsible for sending the session data to the backend with every request. Common SDKs used for this
8275pattern are [auth0-spa-js](https://github.com/auth0/auth0-spa-js) and [auth0-react](https://github.com/auth0/auth0-react).
8276
8277By default, Auth0 stores user session data in JavaScript memory, which Meticulous cannot access while recording sessions. When Meticulous
8278tries to simulate these sessions, Auth0 will fail to refresh the session data and then will force a redirect to the login screen.
8279
8280To 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)
8281for more information on how to set this configuration.
8282
8283### Is the user session managed in the backend?
8284
8285This integration pattern is most common in traditional web applications and in web applications that make heavy use of server-side rendering.
8286In these methods, the user session is managed in the backend, and the backend exposes this 
8286session to the frontend via cookies or headers.
8287Common SDKs used for this pattern are [auth0-node](https://github.com/auth0/node-auth0) and [nextjs-auth0](https://github.com/auth0/nextjs-auth0).
8288
8289By default, Auth0 stores user session data in httpOnly cookies, which Meticulous cannot access while recording sessions. When Meticulous
8290tries to replay these sessions, Meticulous will not pass a session cookie when attempting to load the initial page, so Auth0 will force
8291a redirect to the login screen.
8292
8293To fix this, you need to configure Auth0 to not use httpOnly cookies in the environments where Meticulous records sessions. This can be
8294accomplished by setting the \`AUTH0_COOKIE_HTTP_ONLY\` environment variable to false in the desired environments. See Auth0's documentation
8295[here](https://auth0.github.io/nextjs-auth0/types/config.ConfigParameters.html) for more information on how to set this configuration.
8296
8297### Is the same Auth0 client being used across the record and replay environments?
8298
8299If you have made the suggested changes from the previous sections and are still seeing auth issues, then it is possible that the Auth0
8300client is different between the record and simulation environments. Because live requests are being sent to Auth0 at simulation time
8301with session data collected at record time, the same Auth0 client must be used across record and simulation environments.
8302
8303Please standardize your Auth0 client across environments, and try recording and simulating a session again.
8304
8305{% /tab %}
8306
8307{% tab label="Other" %}
8308
8309## Other
8310
8311### Is the same auth provider client being used across the record and replay environments?
8312
8313Because live requests are often sent to auth providers at simulation time with session data collected at record time, the same auth provider
8314client must be used across record and simulation environments.
8315
8316Please standardize your auth provider client across environments, and try recording and simulating a session again.
8317
8318### Does your backend expose user session data to the browser exclusively via httpOnly cookies?
8319
8320If your backend exclusively uses httpOnly cookies to expose user session data to the browser, then Meticulous will not be able to access
8321this data at record time. This will prevent Meticulous from passing a valid session cookie when loading the initial page at simulation time.
8322
8323To fix this, please disable httpOnly cookies in the environments where Meticulous records sessions.
8324
8325{% /tab %}
8326
8327{% /tabs %}
8328
8329## Issues / questions?
8330
8331We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
8332
8333Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
8334
8335`,eQ=`---
8336{
8337  "title": "Bypassing Auth"
8338}
8339---
8340
8341# {% $frontmatter.title %}
8342
8343If you are only using Meticulous to test your frontend, then Meticulous won't actually need to authenticate with your backend: it'll automatically
8344stub all network responses. However, it may get tripped up if it gets redirected to a login page when trying to simulate a session.
8345
8346You 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.
8347
8348This 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
8349being 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
8350request headers that Meticulous sets](${o.METICULOUS_WINDOW_OBJECT_URL}).
8351
8352## Issues / questions?
8353
8354We're always happy to help you with any issues you encounter while setting up or with anything else you might be unsure about.
8355
8356Get in touch by emailing [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}).
8357
8358`;var eZ=e.i(646846);let e0=(0,eZ.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';
8359import { AppModule } from './app/app.module';
8360import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8361${e}
8362async function startApp() {${t}
8363
8364    // Initialise app after the Meticulous recorder is ready, e.g.
8365    platformBrowserDynamic().bootstrapModule(AppModule)
8366        .catch(err => console.error(err));
8367}
8368
8369function isProduction() {
8370    // TODO: Update me with your production hostname
8371    return window.location.hostname.indexOf("your-production-site.com") > -1;
8372}
8373
8374startApp();
8375`}),e1=(0,eZ.recorderLoaderInstructions)({title:"Installing on Vue",appEntryPointDescription:"app entry point",appEntryPointExampleFileName:"`main.js` or `main.ts`",snippetTemplate:({constants:e,launchRecorderCode:t})=>`import Vue from "vue";
8376import App from "./App.vue";
8377import router from "./router";
8378import store from "./store";
8379import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8380${e}
8381async function startApp() {${t}
8382
8383    // Initialise app after the Meticulous recorder is ready, e.g.
8384    new Vue({
8385      router,
8386      store,
8387      render: h => h(App)
8388    }).$mount("#app");
8389}
8390
8391function isProduction() {
8392    // TODO: Update me with your production hostname
8393    return window.location.hostname.indexOf("your-production-site.com") > -1;
8394}
8395
8396startApp();
8397`}),e2=`If you have any issues setting up the recorder then click [here](${p.METICULOUS_SETUP_CALENDLY_LINK}) to book a call with us.`,e3=`
8398If 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. ${e2}
8399
8400${W}
8401`,e5=`---
8402{
8403  "title": "Set up session recording using an NPM dependency"
8404}
8405---
8406
8407# {% $frontmatter.title %}
8408
8409{% callout_card variant="warning" title="Script tag is the recommended installation method" %}
8410We 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.
8411{% /callout_card %}
8412
8413{% anchor id="${o.INSTALLATION_INSTRUCTIONS_ANCHOR}" /%}
8414
8415Please select your framework:
8416
8417{% tabs direction="grid" noTabSelectedByDefault=true %}
8418{% tab label="Angular" %}
8419${e0}
8420
8421${e3}
8422{% /tab %}
8423{% tab label="Vue" %}
8424${e1}
8425
8426${e3}
8427{% /tab %}
8428
8429{% tab label="React or any other framework" %}
8430${(0,eZ.recorderLoaderInstructions)({title:"Installing on any other framework",appEntryPointDescription:"app entry point",appEntryPointExampleFileName:"`index.js` or `main.js`",snippetTemplate:eZ.anyOtherFrameworkSnippet})}
8431
8432${e3}
8433{% /tab %}
8434
8435{% /tabs %}
8436`,e4=`---
8437{
8438  "title": "Ingest Existing Tests"
8439}
8440---
8441
8442# {% $frontmatter.title %}
8443
8444If 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.
8445
8446This can be particularly useful if existing tests are flaky, have no visual snapshots or have limited visual snapshots.
8447Meticulous 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.
8448
8449## How to setup
8450
84511. Ensure the recorder is available on the page while your tests run - see [Recorder Installation](${o.INSTALL_RECORDER_URL}).
84522. 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.
84533. 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'.
8454`,e6=`---
8455{
8456  "title": "Controlling the data recorded by the Meticulous recorder"
8457}
8458---
8459
8460# {% $frontmatter.title %}
8461
8462There are two main levers you have to control the data and sessions which are collected:
8463
8464 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.
8465 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.
8466`,e7="https://snippet.meticulous.ai/record/v1/network-recorder.bundle.js",e8="network-recorder.bundle.js",e9="via-npm-dependency",te="stop-recording-mid-session",tt=`---
8467{
8468  "title": "Controlling when recording starts and stops"
8469}
8470---
8471
8472# {% $frontmatter.title %}
8473
8474If you already server-side render your initial HTML, and have sufficient information when rendering the initial HTML to determine whether the
8475Meticulous recorder should record the session, then you can conditionally pre-render the Meticulous recorder script tag into your initial HTML.
8476In this case you can stop reading here.
8477
8478If however you need to make frontend web requests to determine whether to start recording (for example fetching user data from an API), then you
8479can use [${e8}](${e7}).
8480
8481Meticulous needs to be able to record all network requests & responses from the very
8482start of your page load for a session to replay correctly. That means that if you only want to record sessions for certain users with
8483certain attributes then you have an issue: you need to wait for the user information to load before you know whether you can enable the
8484recorder, but if you enable the recorder after the user information has loaded then the recorder won't be able to capture the initial
8485request & response to load the user information, or other early network responses.
8486
8487[${e8}](${e7}) solves this: you include it in the HTML originally returned from the server for all sessions,
8488 and it'll temporarily record any network requests in memory (but _not_ send them to the server).
8489
8490If when you load the user data you find out
8491 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
8492 \`@alwaysmeticulous/recorder-loader\`, and any recorded data will be discarded.
8493
8494 If when you load the user
8495data 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)
8496from \`@alwaysmeticulous/recorder-loader\`, at which point the data will start getting sent to the Meticulous servers. You can also
8497[stop recording part way through a session](#${te}), whichever way you have installed the recorder.
8498
8499{% anchor id="via-script-tag" /%}
8500## Setting up conditional recording using ${e8}
8501
8502### Step 1: Add the ${e8} script tag
8503
8504${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](#${e9}).`})}
8505
8506Add the [${e8}](${e7}) script tag to your index.html, or the HTML returned from your server:
8507
8508\`\`\`html
8509<head>
8510  ...
8511  <script
8512    src="${e7}">
8513  </script>
8514
8515  <!-- ${e8} should be added before your app -->
8516  ...
8517  <script src="main_app.js"></script>
8518</head>
8519\`\`\`
8520
8521
8522### Step 2: Conditionally start recording
8523
8524Load the data you need to determine whether to start recording. If you wish to record the session and start sending data to Meticulous then
8525call \`tryLoadAndStartRecorder()\`, otherwise call \`stopIntercepting()\`:
8526
8527{% code_with_project_selector %}
8528\`\`\`typescript
8529import { tryLoadAndStartRecorder, stopIntercepting } from "@alwaysmeticulous/recorder-loader";
8530
8531...
8532
8533const user = await loadUser();
8534if (isNotProduction() && shouldRecord(user)) {
8535  // Note: all errors are caught and logged, so no need to surround with try/catch
8536  await tryLoadAndStartRecorder({
8537    recordingToken: '{% project_recording_token /%}',
8538    isProduction: false,
8539  });
8540} else {
8541  await stopIntercepting();
8542}
8543\`\`\`
8544{% /code_with_project_selector %}
8545
8546{% anchor id="${te}" /%}
8547## Stop recording in the middle of the session
8548
8549Once recording has started you can stop it at any point, whether you installed the recorder as an NPM package or as a script
8550tag. 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
8551user has navigated into an area of your app that you'd rather not capture.
8552
8553{% callout_card showIcon=false %}
8554**Stopping recording is permanent for the current page load**
8555
8556Recording cannot be restarted after it has been stopped, unless the page is reloaded. Everything already uploaded is kept, and
8557the session is flagged in Meticulous as having been stopped by your application. Anything recorded since the last upload is
8558discarded, and no further data is sent to Meticulous's servers.
8559{% /callout_card %}
8560
8561{% tabs %}
8562{% tab label="NPM package" %}
8563\`tryLoadAndStartRecorder()\` resolves to a recorder object with a \`stopRecording()\` method on it. Hold onto that object so that
8564you can stop recording later on:
8565
8566{% code_with_project_selector %}
8567\`\`\`typescript
8568import { tryLoadAndStartRecorder } from "@alwaysmeticulous/recorder-loader";
8569
8570// Start the Meticulous recorder before you initialise your app.
8571// Note: all errors are caught and logged, so no need to surround with try/catch
8572const recorder = await tryLoadAndStartRecorder({
8573  recordingToken: '{% project_recording_token /%}',
8574  isProduction: false,
8575});
8576
8577// ...then later, at any point during the session:
8578await recorder.stopRecording();
8579\`\`\`
8580{% /code_with_project_selector %}
8581
8582If you are using \`tryInstallMeticulousIntercepts()\` instead then call the \`stopRecording()\` method returned by
8583\`startRecordingSession()\` in the same way.
8584{% /tab %}
8585{% tab label="Script tag" %}
8586There is no recorder object to hold onto when using a script tag, so instead call \`stopRecording()\` on the
8587\`window.Meticulous\` API that the recorder script sets up when it initialises:
8588
8589\`\`\`typescript
8590window.Meticulous?.record?.stopRecording();
8591\`\`\`
8592
8593Guard the call as above: \`window.Meticulous\` is not defined if the recorder script failed to load, or if recording was
8594disabled for this page load (for example by setting \`window.METICULOUS_DISABLED\`, or by adding a
8595\`?_meticulousDisabled=true\` query parameter to the URL). \`record\` is likewise absent while Meticulous is replaying the
8596session as a test, when there is nothing to stop -- in TypeScript, narrow on \`isRunningAsTest\` to reach it:
8597
8598\`\`\`typescript
8599if (window.Meticulous && !window.Meticulous.isRunningAsTest) {
8600  window.Meticulous.record.stopRecording();
8601}
8602\`\`\`
8603{% /tab %}
8604{% /tabs %}
8605
8606{% anchor id="${e9}" /%}
8607## Alternative: using an NPM dependency
8608
8609If 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
8610), instead of [${e8}](${e7}). However this is not recommended since it's
8611 easy to miss network requests if libraries you use snapshot references to \`window.fetch\` or \`window.XMLHttpRequest\` early in the page
8612 lifecycle ([learn more](${o.ENSURE_RECORDER_CAPTURES_ALL_REQUESTS_URL})).
8613
8614If when you load the user data you find out you _don't_ want to re
8614cord the session then you can call the
8615\`stopRecording()\` method returned by \`tryInstallMeticulousIntercepts()\`, and any recorded data will be discarded. If when you load the user
8616data you find out you _do_ want to record the session then you can call the \`startRecordingSession()\` method returned
8617by \`tryInstallMeticulousIntercepts()\`, at which point the data will start getting sent to the Meticulous servers. You can then stop
8618recording a session at any point by calling the \`stopRecording()\` method returned by \`startRecordingSession()\`.
8619
8620 Note: \`tryInstallMeticulousIntercepts\` will return a successful promise even if for some reason the browser is unable
8621 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.
8622`,ts=`---
8623{
8624  "title": "Redacting/filtering data before it leaves the browser"
8625}
8626---
8627
8628# {% $frontmatter.title %}
8629
8630By default Meticulous will redact any data entered into a password field from both the user input recorded (keystrokes etc.) and the value
8631from the network requests and responses recorded. You can add additional redaction rules by passing in middleware when initializing the
8632Meticulous recorder. These can be used via a [library of helper functions](https://github.com/alwaysmeticulous/meticulous-sdk/tree/main/packages/redaction)
8633we provide for common cases, for example:
8634
8635\`\`\`typescript
8636import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8637import { dropRequestHeader, transformJsonResponse, redactRecursively, asterixOut } from "@alwaysmeticulous/redaction";
8638
8639...
8640
8641const middleware = [
8642  dropRequestHeader("Authorization"),
8643  transformJsonResponse({
8644    urlRegExp: /https:\\/\\/api\\.example\\.com\\/sensitive.*/,
8645    transform: (data) => redactRecursively(data, {
8646      redactString: str => asterixOut(str),
8647    }),
8648  }),
8649];
8650
8651await tryLoadAndStartRecorder({
8652  recordingToken: '<your recording token>',
8653  middleware
8654});
8655\`\`\`
8656
8657Or directly by writing custom transformation functions:
8658
8659\`\`\`typescript
8660import { tryLoadAndStartRecorder } from '@alwaysmeticulous/recorder-loader'
8661
8662...
8663
8664await tryLoadAndStartRecorder({
8665  recordingToken: '<your recording token>',
8666  middleware: [
8667    {
8668      transformNetworkResponse: (response, metadata) => {
8669        if (!metadata.request.url.endsWith("get-credit-card-details")) {
8670          return response;
8671        }
8672        return {
8673          ...response,
8674          content: {
8675            ...response.content,
8676            text: JSON.stringify({ creditCardNumber: "REDACTED" }),
8677          }
8678        };
8679      }
8680    }
8681  ]
8682})
8683\`\`\`
8684
8685The 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).
8686There 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.
8687
8688In addition to redacting the network requests, responses and application state, you will also need to add the \`${i.METICULOUS_REDACT_RECORDING_CLASS}\`
8689class to any elements that contain data you do not want to record. This will:
8690
8691 1. Stop Meticulous recording the data inside the element in DOM snapshots. These are used for the video replays of the recorded sessions.
8692 2. Stop Meticulous recording text inside the element to identify the elements clicked on when the user clicks on an element.
8693 3. Stop Meticulous from recording keyboard events sent to any widget inside the element.
8694
8695You can use the \`${i.METICULOUS_MASK_RECORDING_PREVIEW_CLASS}\` class to stop (1) without stopping (2) and (3).
8696
8697Please reach out to [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL}) before implementing custom redaction. We can help
8698make sure it is implemented in a way that still allows comprehensive test coverage of all edge cases for your codebase.
8699`,to=`---
8700{
8701  "title": "Next.js App Router - Complete Setup Guide"
8702}
8703---
8704
8705# {% $frontmatter.title %}
8706
8707Complete guide for setting up Meticulous with Next.js applications using the App Router (Next.js 13+).
8708
8709---
8710
8711## Overview
8712
8713Next.js App Router introduces React Server Components, server actions, and streaming - all of which require special handling for automated testing. This guide covers:
8714
8715- **Recorder installation** in App Router layout
8716- **CI/CD configuration** with companion assets optimization
8717- **Server component testing** with deterministic rendering
8718- **Common patterns** for authentication, data fetching, and more
8719
8720**Prerequisites**:
8721- Next.js 13+ using App Router
8722- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
8723
8724---
8725
8726## Quick Start
8727
8728### 1. Install Recorder in Root Layout
8729
8730Add the Meticulous recorder script to your root layout **before any other scripts**.
8731
8732**File**: \`app/layout.tsx\`
8733
8734\`\`\`typescript
8735import type { Metadata } from 'next'
8736
8737export const metadata: Metadata = {
8738  title: 'Your App',
8739  description: 'Your app description',
8740}
8741
8742export default function RootLayout({
8743  children,
8744}: {
8745  children: React.ReactNode
8746}) {
8747  return (
8748    <html lang="en">
8749      <head>
8750        {/* Meticulous recorder - MUST be first script */}
8751        {/* Replace YOUR_PROJECT_ID with your project ID from the dashboard */}
8752        <script
8753          data-project-id="YOUR_PROJECT_ID"
8754          src="https://snippet.meticulous.ai/v1/meticulous.js"
8755        />
8756      </head>
8757      <body>{children}</body>
8758    </html>
8759  )
8760}
8761\`\`\`
8762
8763**Important**: The recorder must load before Next.js client-side JavaScript to capture all events.
8764
8765### 2. Configure GitHub Actions Workflow
8766
8767The 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.
8768
8769**File**: \`.github/workflows/meticulous.yml\`
8770
8771\`\`\`yaml
8772${g}
8773
8774      - uses: docker/setup-buildx-action@v3
8775
8776      - name: Build Docker image
8777        uses: docker/build-push-action@v6
8778        with:
8779          context: .
8780          tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
8781          platforms: linux/amd64
8782          push: false
8783          load: true
8784
8785      - name: Run Meticulous tests
8786        uses: alwaysmeticulous/report-diffs-action/upload-container@v1
8787        with:
8788          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
8789          image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
8790          # Optional: set if your container does not respect the PORT env var
8791          container-port: 3000
8792          # Optional: extra runtime env vars for the container
8793          container-env: |
8794            NODE_ENV=production
8795\`\`\`
8796
8797Your Dockerfile should:
8798- Build for \`linux/amd64\`
8799- Run \`next start\` (or equivalent) in the foreground
8800- Listen on the \`PORT\` env var (or set \`container-port\` to match)
8801- Respond \`2xx\` to a health-check endpoint (defaults to \`GET /\`; override with \`container-health-check-endpoint\`)
8802
8803If 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.
8804
8805### 3. Add API Token Secret
8806
88071. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
88082. Go to GitHub repo → Settings → Secrets and variables → Actions
88093. Create secret named \`METICULOUS_API_TOKEN\` with your token
8810
8811### 4. Configure Project Settings
8812
8813Visit your project settings in Meticulous dashboard:
8814
88151. Go to **Network Stubbing** section
88162. Select: **"Stub all requests, apart from requests for server components and static assets"**
88173. Save settings
8818
8819This ensures React Server Components work correctly during test replay.
8820
8821---
8822
8823## Network Stubbing for Server Components
8824
8825### How It Works
8826
8827Next.js App Router makes requests to itself for Server Components (RSC protocol). These requests look like:
8828
8829\`\`\`
8830GET /?_rsc=123abc
8831\`\`\`
8832
8833**Meticulous behavior**:
8834- **Client-side API calls**: Automatically stubbed with recorded responses
8835- **Server Component requests**: Passed through to running app (not stubbed)
8836- **Static assets**: Served directly by your app (not stubbed)
8837
8838### Why This Matters
8839
8840Server Components render on the server and stream HTML to the client. Stubbing these requests would break the App Router's streaming architecture.
8841
8842**Solution**: Configure network stubbing (step 4 above) to allow Server Component requests through while stubbing external APIs.
8843
8844---
8845
8846## Ensuring Deterministic Rendering
8847
8848{% anchor id="${o.NEXTJS_APP_ROUTER_ENSURING_DETERMINISM_ANCHOR}" /%}
8849
8850### Why Determinism Matters
8851
8852Meticulous 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.
8853
8854**Common sources of non-determinism in Server Components**:
8855- \`Math.random()\`
8856- \`Date.now()\` or \`new Date()\`
8857- \`crypto.randomUUID()\`
8858- External API calls with changing data
8859
8860---
8861
8862### Handling Math.random() in Server Components
8863
8864If you use \`Math.random()\` in Server Components, prefer a request-scoped deterministic random helper:
8865
8866**Install dependency**:
8867
8868\`\`\`bash
8869npm install seedrandom
8870\`\`\`
8871
8872**Create \`lib/random.ts\`**:
8873
8874\`\`\`typescript
8875import { headers } from 'next/headers';
8876import { alea } from 'seedrandom';
8877
8878export const createDeterministicRandom = async (): Promise<() => number> => {
8879  const requestHeaders = await headers();
8880  const isMeticulousTest = requestHeaders.get('meticulous-is-test') === '1';
8881
8882  if (!isMeticulousTest) {
8883    return Math.random;
8884  }
8885
8886  const seed =
8887    requestHeaders.get('meticulous-simulated-date') ?? 'meticulous-test';
8888  const random = alea(seed);
8889
8890  return () => random();
8891};
8892\`\`\`
8893
8894**Requirements**:
8895- \`seedrandom\` package
8896
8897**How it works**:
88981. Meticulous sets \`meticulous-is-test: 1\` header during tests
88992. If you've configured a simulated date header in your Meticulous project settings (using the Simulated Date template), it provides a deterministic seed
89003. Calls to your helper return predictable values during tests
89014. Production behavior unchanged
8902
8903---
8904
8905### Handling Timestamps in Server Components
8906
8907For time-based rendering (e.g., "Posted 5 minutes ago"), you can configure Meticulous to send a simulated date header. Go to your
8908project settings (Settings > Custom Request Headers), add a header named \`meticulous-simulated-date\`, and select the **Simulated Date**
8909template for the value. Then use it in your server code:
8910
8911\`\`\`typescript
8912import { headers } from 'next/headers'
8913
8914const getCurrentDate = async (): Promise<Date> => {
8915  const requestHeaders = await headers()
8916
8917  // If a simulated date header is configured in Meticulous project settings, use it for determinism
8918  const simulatedDate = requestHeaders.get('meticulous-simulated-date')
8919
8920  if (simulatedDate) {
8921    return new Date(Date.parse(simulatedDate))
8922  }
8923
8924  return new Date()
8925}
8926
8927// Usage in Server Component
8928export default async function PostCard() {
8929  const currentDate = await getCurrentDate()
8930  const timeSincePost = calculateTimeDiff(post.createdAt, currentDate)
8931
8932  return (
8933    <div>
8934      <p>Posted {timeSincePost} ago</p>
8935    </div>
8936  );
8937}
8938\`\`\`
8939
8940**Format**: The simulated date is in RFC 7231 format (e.g., \`"Fri, 17 May 2024 13:35:20 GMT"\`)
8941
8942---
8943
8944### Testing Determinism Locally
8945
8946Use a browser extension to simulate Meticulous headers:
8947
89481. Install [ModHeader](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj)
89492. Add headers:
8950   - \`meticulous-is-test\`: \`1\`
8951   - If you've configured a simulated date header, add it too (e.g. \`meticulous-simulated-date\`: \`Fri, 17 May 2024 13:35:20 GMT\`)
89523. Visit your page and reload multiple times
89534. Content should be identical on each reload
8954
8955---
8956
8957### Alternative: Ignore Changing Elements
8958
8959If you can't make an element deterministic, ignore it in screenshots:
8960
8961**Option 1: Add CSS class**
8962
8963\`\`\`typescript
8964<div className="meticulous-ignore">
8965  Posted {timeSincePost} ago
8966</div>
8967\`\`\`
8968
8969**Option 2: Configure in project settings**
8970
8971Go to Project Settings → Screenshots & Flakes → Add CSS selectors to ignore:
8972
8973\`\`\`
8974.timestamp
8975.relative-time
8976[data-testid="posted-time"]
8977\`\`\`
8978
8979Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
8980
8981---
8982
8983## Complete Setup Example
8984
8985### File Structure
8986
8987\`\`\`
8988your-app/
8989├── app/
8990│   ├── layout.tsx              # Recorder installation
8991│   ├── page.tsx                # Server Component
8992│   └── components/
8993│       └── client-component.tsx
8994├── instrumentation.ts          # Math.random() seeding
8995├── lib/
8996│   └── date-utils.ts           # getCurrentDate helper
8997├── .github/
8998│   └── workflows/
8999│       └── meticulous.yml      # CI/CD
9000└── package.json
9001\`\`\`
9002
9003### Example: Server Component with Deterministic Rendering
9004
9005**File**: \`lib/date-utils.ts\`
9006
9007\`\`\`typescript
9008import { headers } from 'next/headers'
9009
9010export const getCurrentDate = async (): Promise<Date> => {
9011  const requestHeaders = await headers()
9012
9013  // If you've configured a simulated date header in Meticulous project settings, use it for determinism
9014  const simulatedDate = requestHeaders.get('meticulous-simulated-date')
9015  return simulatedDate ? new Date(Date.parse(simulatedDate)) : new Date()
9016}
9017
9018export const isMeticulousTest = async (): Promise<boolean> => {
9019  const requestHeaders = await headers()
9020  return requestHeaders.get('meticulous-is-test') === '1'
9021}
9022\`\`\`
9023
9024**File**: \`app/posts/[id]/page.tsx\`
9025
9026\`\`\`typescript
9027import { getCurrentDate, isMeticulousTest } from '@/lib/date-utils'
9028import { formatDistanceToNow } from 'date-fns'
9029
9030interface Post {
9031  id: string
9032  title: string
9033  content: string
9034  createdAt: Date
9035  author: {
9036    name: string
9037    avatar: string
9038  }
9039}
9040
9041async function getPost(id: string): Promise<Post> {
9042  // This fetch is automatically stubbed by Meticulous
9043  const res = await fetch(\`https://api.example.com/posts/\${id}\`)
9044  return res.json()
9045}
9046
9047export default async function PostPage({
9048  params,
9049}: {
9050  params: Promise<{ id: string }>
9051}) {
9052  const { id } = await params
9053  const post = await getPost(id)
9054  const currentDate = await getCurrentDate()
9055
9056  // Calculate time difference using deterministic date
9057  const timeAgo = formatDistanceToNow(post.createdAt, {
9058    addSuffix: true,
9059    includeSeconds: false
9060  })
9061
9062  return (
9063    <article>
9064      <h1>{post.title}</h1>
9065
9066      <div className="author-info">
9067        <img src={post.author.avatar} alt={post.author.name} />
9068        <div>
9069          <p>{post.author.name}</p>
9070          <p className="text-gray-500">
9071            Posted {timeAgo}
9072          </p>
9073        </div>
9074      </div>
9075
9076      <div className="content">
9077        {post.content}
9078      </div>
9079
9080      {(await isMeticulousTest()) && (
9081        /* Show test indicator in tests */
9082        <div className="bg-yellow-100 p-2">Running as test</div>
9083      )}
9084    </article>
9085  )
9086}
9087\`\`\`
9088
9089---
9090
9091## Common Patterns
9092
9093### Pattern 1: Detect Test Mode in Server Components
9094
9095\`\`\`typescript
9096import { headers } from 'next/headers'
9097
9098export default async function Page() {
9099  const requestHeaders = await headers()
9100  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9101
9102  if (isTest) {
9103    // Skip expensive operations during tests
9104    // Or use mock data
9105  }
9106
9107  return <div>...</div>
9108}
9109\`\`\`
9110
9111### Pattern 2: Bypass Authentication in Tests
9112
9113\`\`\`typescript
9114import { headers } from 'next/headers'
9115import { redirect } from 'next/navigation'
9116
9117export default async function ProtectedPage() {
9118  const requestHeaders = await headers()
9119  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9120
9121  if (!isTest) {
9122    const session = await getServerSession()
9123    if (!session) {
9124      redirect('/login')
9125    }
9126  }
9127
9128  // Render protected content
9129  return <div>Protected content</div>
9130}
9131\`\`\`
9132
9133### Pattern 3: Use Mock Data for Tests
9134
9135\`\`\`typescript
9136import { headers } from 'next/headers'
9137
9138async function getData() {
9139  const requestHeaders = await headers()
9140  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9141
9142  if (isTest) {
9143    // Return deterministic test data
9144    return {
9145      id: 'test-id-123',
9146      name: 'Test User',
9147      createdAt: new Date('2024-01-01T00:00:00Z')
9148    }
9149  }
9150
9151  // Fetch real data
9152  const res = await fetch('https://api.example.com/data')
9153  return res.json()
9154}
9155\`\`\`
9156
9157### Pattern 4: Handle Feature Flags
9158
9159\`\`\`typescript
9160import { headers } from 'next/headers'
9161
9162async function getFeatureFlags() {
9163  const requestHeaders = await headers()
9164  const isTest = requestHeaders.get('meticulous-is-test') === '1'
9165
9166  if (isTest) {
9167    // Use deterministic flags during tests
9168    return {
9169      newCheckout: true,
9170      experimentalUI: false
9171    }
9172  }
9173
9174  // Fetch real flags from feature flag service
9175  return await fetchFlags()
9176}
9177\`\`\`
9178
9179---
9180
9181## CI/CD Configuration Details
9182
9183{% callout type="info" title="The sections below apply to the cloud-compute (tunnel) path only" %}
9184If 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\`.
9185{% /callout %}
9186
9187### Why Companion Assets?
9188
9189Next.js static assets (\`/_next/static/\`) are large and numerous. Serving them through the tunnel is slow.
9190
9191**Without companion assets**:
9192\`\`\`
9193Test Duration: ~5 minutes
9194Tunnel Traffic: ~50MB per test run
9195\`\`\`
9196
9197**With companion assets**:
9198\`\`\`
9199Test Duration: ~2 minutes
9200Tunnel Traffic: ~5MB per test run
9201Performance: 60% faster
9202\`\`\`
9203
9204### Companion Assets Setup
9205
9206**Step 1: Copy static files after build**
9207
9208\`\`\`yaml
9209- name: Build Next.js app
9210  run: npm run build
9211
9212- name: Prepare companion assets
9213  run: |
9214    mkdir -p companion-assets/_next
9215    cp -r .next/static companion-assets/_next/
9216    ls -la companion-assets  # Verify files copied
9217\`\`\`
9218
9219**Step 2: Configure Meticulous action**
9220
9221\`\`\`yaml
9222- name: Run Meticulous tests
9223  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
9224  with:
9225    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9226    app-url: "http://localhost:3000"
9227    companion-assets-folder: "companion-assets"
9228    companion-assets-regex: "^/_next/static/"
9229\`\`\`
9230
9231**How it works**:
92321. Meticulous intercepts requests matching \`^/_next/static/\`
92332. Files are served from \`companion-assets/_next/static/\`
92343. Other requests go through tunnel to running app
9235
9236---
9237
9238### Environment Variables
9239
9240**Build-time variables** (prefixed with \`NEXT_PUBLIC_\`):
9241
9242\`\`\`yaml
9243- name: Build Next.js app
9244  run: npm run build
9245  env:
9246    NEXT_PUBLIC_API_URL: "http://localhost:3000/api"
9247    NODE_ENV: production
9248\`\`\`
9249
9250**Runtime variables** (server-side only):
9251
9252\`\`\`yaml
9253- name: Start app
9254  run: npm start &
9255  env:
9256    DATABASE_URL: "postgresql://..."
9257    API_SECRET: \${{ secrets.API_SECRET }}
9258\`\`\`
9259
9260---
9261
9262## Troubleshooting
9263
9264### Issue: "Failed to fetch RSC payload"
9265
9266**Symptom**: Console errors about RSC fetch failures
9267
9268**Cause**: Network stubbing is blocking Server Component requests
9269
9270**Fix**: Update project settings to allow Server Component requests (see step 4 in Quick Start)
9271
9272---
9273
9274### Issue: Timestamps Cause False Positives
9275
9276**Symptom**: Diffs showing "Posted 5 min ago" vs "Posted 6 min ago"
9277
9278**Causes**:
92791. Not using a simulated date header for server-side rendering
92802. Using \`Date.now()\` or \`new Date()\` in Server Components
9281
9282**Fix**: Configure a simulated date header in Meticulous project settings using the Simulated Date template, then use the \`getCurrentDate()\` helper (see Handling Timestamps section)
9283
9284---
9285
9286### Issue: Math.random() Produces Different Results
9287
9288**Symptom**: Random UUIDs, shuffled arrays, or randomized content causes diffs
9289
9290**Fix**: Install seedrandom and setup instrumentation.ts (see Handling Math.random() section)
9291
9292---
9293
9294### Issue: Companion Assets Not Loading
9295
9296**Symptom**: Console errors for \`/_next/static/\` files, or slow test runs
9297
9298**Checks**:
92991. Verify folder exists: \`ls -la companion-assets/_next/static\`
93002. Check files were copied: Should see CSS/JS files
93013. Verify regex pattern matches: \`^/_next/static/\` should match \`/_next/static/chunks/123.js\`
93024. Check workflow syntax: Both \`companion-assets-folder\` and \`companion-assets-regex\` required
9303
9304**Debug**:
9305\`\`\`yaml
9306- name: Debug companion assets
9307  run: |
9308    echo "Checking companion assets..."
9309    ls -la companion-assets/_next/static || echo "Directory not found"
9310    find companion-assets -type f | head -10
9311\`\`\`
9312
9313---
9314
9315### Issue: App Doesn't Start in CI
9316
9317**Symptom**: "ECONNREFUSED" or "Failed to connect to http://localhost:3000"
9318
9319**Common causes**:
93201. Build failed silently
93212. Port already in use
93223. Missing environment variables
93234. App requires database connection
9324
9325**Debug steps**:
9326
9327\`\`\`yaml
9328- name: Start app with logging
9329  run: |
9330    npm start > app.log 2>&1 &
9331    sleep 5
9332    cat app.log  # Check for startup errors
9333
9334- name: Verify app is running
9335  run: |
9336    curl http://localhost:3000 || echo "App not responding"
9337    npx wait-on http://localhost:3000 --timeout 60000
9338\`\`\`
9339
9340---
9341
9342### Issue: Authentication Blocks Tests
9343
9344**Symptom**: Tests fail because pages redirect to login
9345
9346**Solutions**:
9347
9348**Option 1: Bypass auth in tests** (recommended)
9349
9350\`\`\`typescript
9351import { headers } from 'next/headers'
9352
9353const isMeticulousTest = async () => {
9354  const requestHeaders = await headers()
9355  return requestHeaders.get('meticulous-is-test') === '1'
9356}
9357
9358export default async function ProtectedLayout({ children }) {
9359  if (!(await isMeticulousTest())) {
9360    const session = await getServerSession()
9361    if (!session) redirect('/login')
9362  }
9363
9364  return <>{children}</>
9365}
9366\`\`\`
9367
9368**Option 2: Mock authentication**
9369
9370\`\`\`typescript
9371if (isMeticulousTest()) {
9372  // Return mock session for tests
9373  return {
9374    user: { id: 'test-user', name: 'Test User' }
9375  }
9376}
9377\`\`\`
9378
9379See full guide: [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL})
9380
9381---
9382
9383## Advanced Configuration
9384
9385### Custom Tunnel Options
9386
9387If you need to proxy multiple ports or use HTTPS:
9388
9389\`\`\`yaml
9390- name: Run Meticulous tests
9391  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
9392  with:
9393    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9394    app-url: "http://localhost:3000"
9395    proxy-all-urls: true  # Proxy all domains, not just app-url
9396    companion-assets-folder: "companion-assets"
9397    companion-assets-regex: "^/_next/static/"
9398\`\`\`
9399
9400See: [Tunnel Advanced Options](${o.TUNNEL_ADVANCED_OPTIONS_URL})
9401
9402---
9403
9404### Monorepo Setup
9405
9406If your Next.js app is in a subdirectory:
9407
9408\`\`\`yaml
9409- name: Install dependencies
9410  working-directory: ./apps/frontend
9411  run: npm ci
9412
9413- name: Build app
9414  working-directory: ./apps/frontend
9415  run: npm run build
9416
9417- name: Prepare companion assets
9418  working-directory: ./apps/frontend
9419  run: |
9420    mkdir -p companion-assets/_next
9421    cp -r .next/static companion-assets/_next/
9422
9423- name: Start app
9424  working-directory: ./apps/frontend
9425  run: npm start &
9426
9427- name: Run Meticulous tests
9428  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
9429  with:
9430    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9431    app-url: "http://localhost:3000"
9432    companion-assets-folder: "./apps/frontend/companion-assets"
9433    companion-assets-regex: "^/_next/static/"
9434\`\`\`
9435
9436---
9437
9438## Testing Best Practices
9439
9440### 1. Record Real User Sessions
9441
9442Record sessions in staging or production (with appropriate privacy controls):
9443
9444\`\`\`typescript
9445// Only load recorder in staging/production
9446const shouldLoadRecorder =
9447  process.env.NEXT_PUBLIC_ENV === 'staging' ||
9448  process.env.NEXT_PUBLIC_ENV === 'production'
9449
9450export default function RootLayout({ children }) {
9451  return (
9452    <html>
9453      <head>
9454        {shouldLoadRecorder && (
9455          <script
9456            data-project-id={process.env.NEXT_PUBLIC_METICULOUS_PROJECT_ID}
9457            src="https://snippet.meticulous.ai/v1/meticulous.js"
9458          />
9459        )}
9460      </head>
9461      <body>{children}</body>
9462    </html>
9463  )
9464}
9465\`\`\`
9466
9467### 2. Curate Your Test Suite
9468
9469Review recorded sessions in the dashboard and select high-value flows:
9470- Critical user journeys (signup, checkout, etc.)
9471- High-traffic pages
9472- Recently changed features
9473- Edge cases
9474
9475### 3. Handle Dynamic Content
9476
9477For content that changes frequently (ads, recommendations, live data):
9478
9479\`\`\`typescript
9480<div className="meticulous-ignore">
9481  {/* Content that changes frequently */}
9482</div>
9483\`\`\`
9484
9485### 4. Test Locally Before CI
9486
9487Run tests locally to catch issues faster:
9488
9489\`\`\`bash
9490# Start your app
9491npm run dev
9492
9493# In another terminal, run Meticulous CLI
9494npx @alwaysmeticulous/cli simulate \\
9495  --sessionId="YOUR_SESSION_ID" \\
9496  --appUrl="http://localhost:3000"
9497\`\`\`
9498
9499---
9500
9501## Migration from Pages Router
9502
9503If you're migrating from Pages Router to App Router:
9504
95051. **Keep recorder in head**: Move from \`_document.tsx\` to \`app/layout.tsx\`
95062. **Update network stubbing**: Enable Server Component request passthrough
95073. **Update Math.random() calls**: Use \`createDeterministicRandom()\` helper in Server Components
95084. **Update date handling**: Use \`getCurrentDate()\` helper in Server Components
95095. **Test thoroughly**: Server Components behave differently than client components
9510
9511---
9512
9513## Example Repository
9514
9515The configuration above is a complete App Router setup.
9516
9517---
9518
9519## See Also
9520
9521- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}
9521) - General Meticulous setup
9522- [Network Stubbing Explanation](${o.NETWORK_STUBBING_EXPLANATION_URL}) - How API mocking works
9523- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
9524- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
9525- [Companion Assets Guide](${o.COMPANION_ASSETS_ADVANCED_URL}) - Deep dive into static asset optimization
9526
9527`,tn=`---
9528{
9529  "title": "Next.js Pages Router - Complete Setup Guide"
9530}
9531---
9532
9533# {% $frontmatter.title %}
9534
9535Complete 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).
9536
9537---
9538
9539## Overview
9540
9541The Pages Router is the traditional Next.js routing system. This guide covers:
9542
9543- **Recorder installation** in \`_document.tsx\`
9544- **CI/CD configuration** with companion assets
9545- **Authentication handling**
9546- **Common patterns** and troubleshooting
9547
9548**Prerequisites**:
9549- Next.js application using Pages Router
9550- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
9551
9552---
9553
9554## Quick Start
9555
9556### Step 1: Install Recorder in _document.tsx
9557
9558Add the Meticulous recorder script to your custom Document component **before any other scripts**.
9559
9560**File**: \`pages/_document.tsx\`
9561
9562\`\`\`typescript
9563import { Html, Head, Main, NextScript } from 'next/document'
9564
9565export default function Document() {
9566  return (
9567    <Html lang="en">
9568      <Head>
9569        {/* Meticulous recorder - MUST be first script */}
9570        {/* Replace YOUR_PROJECT_ID with your project ID from the dashboard */}
9571        <script
9572          data-project-id="YOUR_PROJECT_ID"
9573          src="https://snippet.meticulous.ai/v1/meticulous.js"
9574        />
9575      </Head>
9576      <body>
9577        <Main />
9578        <NextScript />
9579      </body>
9580    </Html>
9581  )
9582}
9583\`\`\`
9584
9585**Important**: The recorder must load before Next.js client-side JavaScript to capture all events.
9586
9587**If you don't have _document.tsx**: Create it in \`pages/_document.tsx\` with the code above.
9588
9589---
9590
9591### Step 2: Configure GitHub Actions Workflow
9592
9593Create a workflow that builds your app and uses companion assets for optimal performance.
9594
9595**File**: \`.github/workflows/meticulous.yml\`
9596
9597\`\`\`yaml
9598${g}
9599
9600      - uses: docker/setup-buildx-action@v3
9601
9602      - name: Build Docker image
9603        uses: docker/build-push-action@v6
9604        with:
9605          context: .
9606          tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
9607          platforms: linux/amd64
9608          push: false
9609          load: true
9610
9611      - name: Run Meticulous tests
9612        uses: alwaysmeticulous/report-diffs-action/upload-container@v1
9613        with:
9614          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
9615          image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
9616          # Optional: set if your container does not respect the PORT env var
9617          container-port: 3000
9618          # Optional: extra runtime env vars for the container
9619          container-env: |
9620            NODE_ENV=production
9621\`\`\`
9622
9623The 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.
9624
9625Your Dockerfile should:
9626- Build for \`linux/amd64\`
9627- Run \`next start\` (or equivalent) in the foreground
9628- Listen on the \`PORT\` env var (or set \`container-port\` to match)
9629- Respond \`2xx\` to a health-check endpoint (defaults to \`GET /\`; override with \`container-health-check-endpoint\`)
9630
9631If 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.
9632
9633---
9634
9635### Step 3: Add API Token Secret
9636
96371. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
96382. Go to GitHub repo → Settings → Secrets and variables → Actions
96393. Create secret named \`METICULOUS_API_TOKEN\` with your token
9640
9641---
9642
9643## Companion Assets
9644
9645{% callout type="info" title="Only relevant if you're using cloud-compute" %}
9646The 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.
9647{% /callout %}
9648
9649### Why Use Companion Assets?
9650
9651Next.js static assets (\`/_next/static/\`) are large and numerous. Serving them through the tunnel is slow.
9652
9653**Performance improvement**:
9654- **Without companion assets**: ~5 minutes test duration
9655- **With companion assets**: ~2 minutes test duration (60% faster)
9656
9657### Setup
9658
9659Add these steps to your \`cloud-compute\` workflow:
9660
9661\`\`\`yaml
9662- name: Prepare companion assets
9663  run: |
9664    mkdir -p companion-assets/_next
9665    cp -r .next/static companion-assets/_next/
9666
9667- name: Run Meticulous tests
9668  with:
9669    companion-assets-folder: "companion-assets"
9670    companion-assets-regex: "^/_next/static/"
9671\`\`\`
9672
9673**How it works**:
96741. Build creates \`.next/static/\` with all static assets
96752. Copy \`.next/static/\` to \`companion-assets/_next/static/\`
96763. Meticulous serves these files directly, bypassing the tunnel
9677
9678Learn more: [Companion Assets Guide](${o.COMPANION_ASSETS_ADVANCED_URL})
9679
9680---
9681
9682## Common Patterns
9683
9684### Pattern 1: Detect Test Mode
9685
9686Use \`window.Meticulous.isRunningAsTest\` to detect when running as a test:
9687
9688\`\`\`typescript
9689// In any component
9690function MyComponent() {
9691  const isTest = window.Meticulous?.isRunningAsTest
9692
9693  if (isTest) {
9694    // Skip animations, use test data, etc.
9695  }
9696
9697  return <div>...</div>
9698}
9699\`\`\`
9700
9701### Pattern 2: Bypass Authentication
9702
9703\`\`\`typescript
9704// pages/_app.tsx
9705import { useEffect } from 'react'
9706import { useRouter } from 'next/router'
9707
9708function MyApp({ Component, pageProps }) {
9709  const router = useRouter()
9710
9711  useEffect(() => {
9712    if (window.Meticulous?.isRunningAsTest) {
9713      // Mock authentication for tests
9714      localStorage.setItem('auth-token', 'test-token')
9715      localStorage.setItem('user', JSON.stringify({
9716        id: 'test-user',
9717        name: 'Test User',
9718        email: '[email protected]'
9719      }))
9720    }
9721  }, [])
9722
9723  return <Component {...pageProps} />
9724}
9725\`\`\`
9726
9727### Pattern 3: Server-Side Detection
9728
9729Check for \`meticulous-is-test\` header in \`getServerSideProps\`:
9730
9731\`\`\`typescript
9732export const getServerSideProps = async (context) => {
9733  const { req } = context
9734  const isTest = req.headers['meticulous-is-test'] === '1'
9735
9736  if (isTest) {
9737    // Skip auth redirect, use test data, etc.
9738    return {
9739      props: {
9740        user: { id: 'test-user', name: 'Test User' }
9741      }
9742    }
9743  }
9744
9745  // Normal server-side logic
9746  const session = await getSession(context)
9747  if (!session) {
9748    return {
9749      redirect: {
9750        destination: '/login',
9751        permanent: false,
9752      }
9753    }
9754  }
9755
9756  return {
9757    props: {
9758      user: session.user
9759    }
9760  }
9761}
9762\`\`\`
9763
9764### Pattern 4: Handle Dynamic Timestamps
9765
9766Ignore elements with frequently changing content:
9767
9768\`\`\`typescript
9769// Add meticulous-ignore class
9770<div className="meticulous-ignore">
9771  Posted {formatDistanceToNow(post.createdAt)} ago
9772</div>
9773\`\`\`
9774
9775Or configure in project settings to ignore CSS selectors globally.
9776
9777---
9778
9779## Complete Example
9780
9781### File Structure
9782
9783\`\`\`
9784your-app/
9785├── pages/
9786│   ├── _app.tsx              # App wrapper
9787│   ├── _document.tsx         # Recorder installation
9788│   ├── index.tsx             # Home page
9789│   └── dashboard.tsx         # Protected page
9790├── lib/
9791│   └── auth.ts               # Auth utilities
9792├── .github/
9793│   └── workflows/
9794│       └── meticulous.yml    # CI/CD
9795└── package.json
9796\`\`\`
9797
9798### Example: Protected Page
9799
9800**File**: \`pages/dashboard.tsx\`
9801
9802\`\`\`typescript
9803import { GetServerSideProps } from 'next'
9804import { getSession } from 'next-auth/react'
9805
9806interface DashboardProps {
9807  user: {
9808    id: string
9809    name: string
9810    email: string
9811  }
9812}
9813
9814export const getServerSideProps: GetServerSideProps<DashboardProps> = async (context) => {
9815  const isTest = context.req.headers['meticulous-is-test'] === '1'
9816
9817  if (isTest) {
9818    // Bypass auth during tests
9819    return {
9820      props: {
9821        user: {
9822          id: 'test-user-123',
9823          name: 'Test User',
9824          email: '[email protected]'
9825        }
9826      }
9827    }
9828  }
9829
9830  // Normal auth flow
9831  const session = await getSession(context)
9832
9833  if (!session) {
9834    return {
9835      redirect: {
9836        destination: '/login?redirect=/dashboard',
9837        permanent: false,
9838      }
9839    }
9840  }
9841
9842  return {
9843    props: {
9844      user: session.user
9845    }
9846  }
9847}
9848
9849export default function Dashboard({ user }: DashboardProps) {
9850  return (
9851    <div>
9852      <h1>Welcome, {user.name}!</h1>
9853      <p>Email: {user.email}</p>
9854    </div>
9855  )
9856}
9857\`\`\`
9858
9859---
9860
9861## CI/CD Configuration Details
9862
9863### Environment Variables
9864
9865**Build-time variables** (prefixed with \`NEXT_PUBLIC_\`):
9866
9867\`\`\`yaml
9868- name: Build Next.js app
9869  run: npm run build
9870  env:
9871    NEXT_PUBLIC_API_URL: "http://localhost:3000/api"
9872    NODE_ENV: production
9873\`\`\`
9874
9875**Runtime variables** (server-side only):
9876
9877\`\`\`yaml
9878- name: Start app
9879  run: npm start &
9880  env:
9881    DATABASE_URL: "postgresql://..."
9882    API_SECRET: \${{ secrets.API_SECRET }}
9883\`\`\`
9884
9885### Custom Build Scripts
9886
9887If you have a custom build process:
9888
9889\`\`\`yaml
9890- name: Build app
9891  run: |
9892    npm run build:custom
9893    npm run postbuild:assets
9894
9895- name: Prepare companion assets
9896  run: |
9897    mkdir -p companion-assets/_next
9898    cp -r .next/static companion-assets/_next/
9899    # Copy any additional static assets
9900    cp -r public/static companion-assets/static
9901\`\`\`
9902
9903---
9904
9905## Troubleshooting
9906
9907### Issue: Recorder Not Loading
9908
9909**Symptom**: \`window.Meticulous\` is undefined
9910
9911**Checks**:
99121. Verify \`_document.tsx\` has recorder script in \`<Head>\`
99132. Check project ID is correct
99143. Check for CSP blocking (console errors)
99154. Verify script loads before other scripts
9916
9917**Fix**: Ensure recorder is in \`<Head>\`, not \`<body>\`:
9918
9919\`\`\`typescript
9920<Head>
9921  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js" />
9922  {/* Other head elements */}
9923</Head>
9924\`\`\`
9925
9926---
9927
9928### Issue: App Doesn't Start in CI
9929
9930**Symptom**: "ECONNREFUSED" or "Failed to connect to http://localhost:3000"
9931
9932**Common causes**:
99331. Build failed silently
99342. Port already in use
99353. Missing environment variables
9936
9937**Debug**:
9938
9939\`\`\`yaml
9940- name: Start app with logging
9941  run: |
9942    npm start > app.log 2>&1 &
9943    sleep 5
9944    cat app.log
9945
9946- name: Verify app is running
9947  run: |
9948    curl http://localhost:3000 || echo "App not responding"
9949    npx wait-on http://localhost:3000 --timeout 60000
9950\`\`\`
9951
9952---
9953
9954### Issue: Authentication Blocks Tests
9955
9956**Symptom**: Tests fail because pages redirect to login
9957
9958**Solution 1: Bypass auth in \`getServerSideProps\`**
9959
9960\`\`\`typescript
9961export const getServerSideProps = (context) => {
9962  const isTest = context.req.headers['meticulous-is-test'] === '1'
9963
9964  if (isTest) {
9965    return { props: { user: mockUser } }
9966  }
9967
9968  // Normal auth flow
9969}
9970\`\`\`
9971
9972**Solution 2: Mock auth in \`_app.tsx\`**
9973
9974\`\`\`typescript
9975useEffect(() => {
9976  if (window.Meticulous?.isRunningAsTest) {
9977    // Set mock auth data
9978    localStorage.setItem('token', 'test-token')
9979  }
9980}, [])
9981\`\`\`
9982
9983See full guide: [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL})
9984
9985---
9986
9987### Issue: Companion Assets Not Loading
9988
9989**Symptom**: Console errors for \`/_next/static/\` files
9990
9991**Checks**:
99921. Verify folder exists: \`ls -la companion-assets/_next/static\`
99932. Check files were copied after build
99943. Verify regex pattern: \`^/_next/static/\`
9995
9996**Fix**: Ensure build completes before copying:
9997
9998\`\`\`yaml
9999- name: Build Next.js app
10000  run: npm run build
10001
10002- name: Verify build output
10003  run: ls -la .next/static
10004
10005- name: Prepare companion assets
10006  run: |
10007    mkdir -p companion-assets/_next
10008    cp -r .next/static companion-assets/_next/
10009    ls -la companion-assets/_next/static
10010\`\`\`
10011
10012---
10013
10014### Issue: False Positive Diffs
10015
10016**Symptom**: Tests show diffs for content that hasn't changed
10017
10018**Common causes**:
100191. Timestamps: "Posted 5 min ago" vs "Posted 6 min ago"
100202. Random IDs or UUIDs
100213. Animations not completing
10022
10023**Fixes**:
10024
10025**Timestamps**: Add \`meticulous-ignore\` class
10026\`\`\`typescript
10027<span className="meticulous-ignore">
10028  Posted {timeAgo} ago
10029</span>
10030\`\`\`
10031
10032**Random IDs**: Use deterministic IDs in tests
10033\`\`\`typescript
10034const generateId = () => {
10035  if (window.Meticulous?.isRunningAsTest) {
10036    return 'test-id-12345'
10037  }
10038  return crypto.randomUUID()
10039}
10040\`\`\`
10041
10042**Animations**: Disable in tests
10043\`\`\`typescript
10044const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
10045\`\`\`
10046
10047Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
10048
10049---
10050
10051## Migration from App Router
10052
10053If you're migrating to Pages Router from App Router:
10054
100551. **Move recorder**: From \`app/layout.tsx\` to \`pages/_document.tsx\`
100562. **Update auth**: Change from \`headers()\` to \`getServerSideProps\`
100573. **Test thoroughly**: Server-side logic differs between routers
10058
10059---
10060
10061## Testing Best Practices
10062
10063### 1. Record Real User Sessions
10064
10065Record sessions in staging or production (with appropriate privacy controls):
10066
10067\`\`\`typescript
10068// Only load recorder in specific environments
10069const shouldLoadRecorder =
10070  process.env.NEXT_PUBLIC_ENV === 'staging' ||
10071  process.env.NEXT_PUBLIC_ENV === 'production'
10072\`\`\`
10073
10074### 2. Handle Dynamic Content
10075
10076For frequently changing content:
10077
10078\`\`\`typescript
10079<div className="meticulous-ignore">
10080  {/* Content that changes frequently */}
10081</div>
10082\`\`\`
10083
10084### 3. Test Locally
10085
10086Run tests locally before CI:
10087
10088\`\`\`bash
10089# Start your app
10090npm run dev
10091
10092# In another terminal
10093npx @alwaysmeticulous/cli simulate \\
10094  --sessionId="YOUR_SESSION_ID" \\
10095  --appUrl="http://localhost:3000"
10096\`\`\`
10097
10098---
10099
10100## Advanced Configuration
10101
10102### Monorepo Setup
10103
10104If your Next.js app is in a subdirectory:
10105
10106\`\`\`yaml
10107- name: Install dependencies
10108  working-directory: ./apps/frontend
10109  run: npm ci
10110
10111- name: Build app
10112  working-directory: ./apps/frontend
10113  run: npm run build
10114
10115- name: Prepare companion assets
10116  working-directory: ./apps/frontend
10117  run: |
10118    mkdir -p companion-assets/_next
10119    cp -r .next/static companion-assets/_next/
10120
10121- name: Start app
10122  working-directory: ./apps/frontend
10123  run: npm start &
10124
10125- name: Run Meticulous tests
10126  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
10127  with:
10128    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10129    app-url: "http://localhost:3000"
10130    companion-assets-folder: "./apps/frontend/companion-assets"
10131    companion-assets-regex: "^/_next/static/"
10132\`\`\`
10133
10134---
10135
10136## See Also
10137
10138- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
10139- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
10140- [Companion Assets Guide](${o.COMPANION_ASSETS_ADVANCED_URL}) - Deep dive into static asset optimization
10141- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
10142`,ti=`---
10143{
10144  "title": "React with Vite - Complete Setup Guide"
10145}
10146---
10147
10148# {% $frontmatter.title %}
10149
10150Complete guide for setting up Meticulous with React applications built with Vite.
10151
10152---
10153
10154## Overview
10155
10156Vite is a fast build tool for modern web applications. This guide covers:
10157
10158- **Recorder installation** in \`index.html\`
10159- **CI/CD configuration** with static asset upload
10160- **Authentication handling**
10161- **Common patterns** and troubleshooting
10162
10163**Prerequisites**:
10164- React application using Vite
10165- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
10166
10167---
10168
10169## Quick Start
10170
10171### Step 1: Install Recorder in index.html
10172
10173Add the Meticulous recorder script to your \`index.html\` **before any other scripts**.
10174
10175**File**: \`index.html\`
10176
10177\`\`\`html
10178<!DOCTYPE html>
10179<html lang="en">
10180  <head>
10181    <meta charset="UTF-8" />
10182    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
10183    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10184    <title>Your App Name</title>
10185
10186    <!-- Meticulous recorder - MUST be first script -->
10187    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
10188    <script
10189      data-project-id="YOUR_PROJECT_ID"
10190      src="https://snippet.meticulous.ai/v1/meticulous.js"
10191    ></script>
10192  </head>
10193  <body>
10194    <div id="root"></div>
10195    <script type="module" src="/src/main.tsx"></script>
10196  </body>
10197</html>
10198\`\`\`
10199
10200**Important**: The recorder must load before your application code to capture all events.
10201
10202---
10203
10204### Step 2: Configure GitHub Actions Workflow
10205
10206Vite builds to static files, so we use the \`upload-assets\` action instead of \`cloud-compute\`.
10207
10208**File**: \`.github/workflows/meticulous.yml\`
10209
10210\`\`\`yaml
10211${g}
10212
10213      - uses: actions/setup-node@v4
10214        with:
10215          node-version: 20
10216          cache: 'npm'
10217
10218      - name: Install dependencies
10219        run: npm ci
10220
10221      - name: Build app
10222        run: npm run build
10223        env:
10224          NODE_ENV: production
10225
10226      - name: Upload and test
10227        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10228        with:
10229          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10230          app-directory: "dist"
10231          rewrites: |
10232            [
10233              { "source": "/(.*)", "destination": "/index.html" }
10234            ]
10235\`\`\`
10236
10237---
10238
10239### Step 3: Add API Token Secret
10240
102411. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
102422. Go to GitHub repo → Settings → Secrets and variables → Actions
102433. Create secret named \`METICULOUS_API_TOKEN\` with your token
10244
10245---
10246
10247## How It Works
10248
10249### upload-assets Action
10250
10251The \`upload-assets\` action:
102521. Uploads your built static files to Meticulous
102532. Serves them on a temporary URL
102543. Runs tests against that URL
102554. Reports diffs back to your PR
10256
10257**Key differences from cloud-compute**:
10258- Simpler setup (no server needed)
10259- Faster for static sites
10260- Can't test server-side logic
10261- No backend API calls (unless mocked)
10262
10263### Rewrites Configuration
10264
10265The \`rewrites\` parameter handles client-side routing:
10266
10267\`\`\`json
10268[
10269  { "source": "/(.*)", "destination": "/index.html" }
10270]
10271\`\`\`
10272
10273This ensures all routes (\`/about\`, \`/dashboard\`, etc.) serve \`index.html\`, allowing React Router to handle routing.
10274
10275---
10276
10277## Common Patterns
10278
10279### Pattern 1: Detect Test Mode
10280
10281Use \`window.Meticulous.isRunningAsTest\` to detect when running as a test:
10282
10283\`\`\`typescript
10284// In any component
10285function MyComponent() {
10286  const isTest = window.Meticulous?.isRunningAsTest
10287
10288  if (isTest) {
10289    // Skip animations, use test data, etc.
10290  }
10291
10292  return <div>...</div>
10293}
10294\`\`\`
10295
10296### Pattern 2: Bypass Authentication
10297
10298\`\`\`typescript
10299// In App.tsx or auth provider
10300import { useEffect } from 'react'
10301
10302function App() {
10303  useEffect(() => {
10304    if (window.Meticulous?.isRunningAsTest) {
10305      // Mock authentication for tests
10306      localStorage.setItem('auth-token', 'test-token')
10307      localStorage.setItem('user', JSON.stringify({
10308        id: 'test-user',
10309        name: 'Test User',
10310        email: '[email protected]'
10311      }))
10312    }
10313  }, [])
10314
10315  return <YourApp />
10316}
10317\`\`\`
10318
10319### Pattern 3: Mock API Responses
10320
10321Since there's no backend in upload-assets mode, API calls need to be mocked:
10322
10323**Option 1: Use MSW (Mock Service Worker)**
10324
10325\`\`\`typescript
10326// src/mocks/browser.ts
10327import { setupWorker } from 'msw/browser'
10328import { handlers } from './handlers'
10329
10330export const worker = setupWorker(...handlers)
10331
10332// src/main.tsx
10333if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
10334  const { worker } = await import('./mocks/browser')
10335  await worker.start()
10336}
10337\`\`\`
10338
10339**Option 2: Use recorded custom values**
10340
10341\`\`\`typescript
10342// During recording
10343window.Meticulous?.recordCustomValues?.({
10344  apiData: await fetchFromAPI()
10345})
10346
10347// During replay
10348const data = window.Meticulous?.isRunningAsTest
10349  ? window.Meticulous.getCustomValues()?.apiData
10350  : await fetchFromAPI()
10351\`\`\`
10352
10353### Pattern 4: Handle Environment Variables
10354
10355Vite exposes environment variables prefixed with \`VITE_\`:
10356
10357\`\`\`typescript
10358const apiUrl = import.meta.env.VITE_API_URL
10359
10360// Use different URL for tests
10361const effectiveUrl = window.Meticulous?.isRunningAsTest
10362  ? 'https://api.test.example.com'
10363  : apiUrl
10364\`\`\`
10365
10366---
10367
10368## Complete Example
10369
10370### File Structure
10371
10372\`\`\`
10373your-app/
10374├── src/
10375│   ├── main.tsx              # Entry point
10376│   ├── App.tsx               # Main app component
10377│   ├── components/
10378│   ├── lib/
10379│   │   └── auth.ts           # Auth utilities
10380│   └── mocks/                # MSW mocks (optional)
10381├── index.html                # Recorder installation
10382├── vite.config.ts            # Vite configuration
10383├── .github/
10384│   └── workflows/
10385│       └── meticulous.yml    # CI/CD
10386└── package.json
10387\`\`\`
10388
10389### Example: Protected Route
10390
10391**File**: \`src/App.tsx\`
10392
10393\`\`\`typescript
10394import { useEffect, useState } from 'react'
10395import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
10396
10397function App() {
10398  const [user, setUser] = useState(null)
10399  const [loading, setLoading] = useState(true)
10400
10401  useEffect(() => {
10402    // Mock auth for tests
10403    if (window.Meticulous?.isRunningAsTest) {
10404      setUser({
10405        id: 'test-user-123',
10406        name: 'Test User',
10407        email: '[email protected]'
10408      })
10409      setLoading(false)
10410      return
10411    }
10412
10413    // Normal auth flow
10414    checkAuth().then(user => {
10415      setUser(user)
10416      setLoading(false)
10417    })
10418  }, [])
10419
10420  if (loading) {
10421    return <div>Loading...</div>
10422  }
10423
10424  return (
10425    <BrowserRouter>
10426      <Routes>
10427        <Route path="/" element={<HomePage />} />
10428        <Route
10429          path="/dashboard"
10430          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
10431        />
10432        <Route path="/login" element={<LoginPage />} />
10433      </Routes>
10434    </BrowserRouter>
10435  )
10436}
10437
10438export default App
10439\`\`\`
10440
10441---
10442
10443## CI/CD Configuration Details
10444
10445### Environment Variables
10446
10447Add build-time environment variables:
10448
10449\`\`\`yaml
10450- name: Build app
10451  run: npm run build
10452  env:
10453    VITE_API_URL: "https://api.example.com"
10454    VITE_APP_NAME: "My App"
10455    NODE_ENV: production
10456\`\`\`
10457
10458**In code**:
10459\`\`\`typescript
10460const apiUrl = import.meta.env.VITE_API_URL
10461\`\`\`
10462
10463### Custom Build Directory
10464
10465If Vite outputs to a different directory:
10466
10467\`\`\`yaml
10468- name: Upload and test
10469  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10470  with:
10471    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10472    app-directory: "build" # Change from default "dist"
10473    rewrites: |
10474      [
10475        { "source": "/(.*)", "destination": "/index.html" }
10476      ]
10477\`\`\`
10478
10479### Multiple Rewrites
10480
10481For complex routing:
10482
10483\`\`\`yaml
10484rewrites: |
10485  [
10486    { "source": "/api/(.*)", "destination": "/api/index.html" },
10487    { "source": "/(.*)", "destination": "/index.html" }
10488  ]
10489\`\`\`
10490
10491---
10492
10493## Troubleshooting
10494
10495### Issue: Recorder Not Loading
10496
10497**Symptom**: \`window.Meticulous\` is undefined
10498
10499**Checks**:
105001. Verify recorder script is in \`index.html\` \`<head>\`
105012. Check project ID is correct
105023. Check for CSP blocking (console errors)
105034. Verify script loads before \`src/main.tsx\`
10504
10505**Fix**: Ensure correct order in \`index.html\`:
10506
10507\`\`\`html
10508<head>
10509  <!-- Recorder FIRST -->
10510  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
10511
10512  <!-- Then other scripts -->
10513</head>
10514<body>
10515  <div id="root"></div>
10516  <script type="module" src="/src/main.tsx"></script>
10517</body>
10518\`\`\`
10519
10520---
10521
10522### Issue: Routes Return 404
10523
10524**Symptom**: Direct navigation to \`/about\` returns 404
10525
10526**Cause**: Missing rewrite configuration
10527
10528**Fix**: Add rewrites to workflow:
10529
10530\`\`\`yaml
10531rewrites: |
10532  [
10533    { "source": "/(.*)", "destination": "/index.html" }
10534  ]
10535\`\`\`
10536
10537---
10538
10539### Issue: API Calls Fail
10540
10541**Symptom**: API requests fail during tests
10542
10543**Cause**: No backend in upload-assets mode
10544
10545**Solutions**:
10546
10547**Option 1: Mock APIs with MSW** (recommended)
10548
10549See Pattern 3 above. Keeps you on \`upload-assets\`, which is the simplest and most reliable workflow.
10550
10551**Option 2: Switch to \`upload-container\`** (if you have a backend you want to actually run)
10552
10553Build 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\`:
10554
10555\`\`\`yaml
10556- uses: docker/setup-buildx-action@v3
10557
10558- uses: docker/build-push-action@v6
10559  with:
10560    context: .
10561    tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
10562    platforms: linux/amd64
10563    push: false
10564    load: true
10565
10566- name: Run Meticulous tests
10567  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
10568  with:
10569    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10570    image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
10571    container-port: 5173
10572\`\`\`
10573
10574---
10575
10576### Issue: Build Fails
10577
10578**Symptom**: \`npm run build\` fails in CI
10579
10580**Common causes**:
105811. TypeScript errors
105822. Missing environment variables
105833. Linting errors treated as build errors
10584
10585**Debug**:
10586
10587\`\`\`yaml
10588- name: Build app
10589  run: npm run build
10590  env:
10591    CI: false # Treats warnings as non-blocking
10592    NODE_ENV: production
10593\`\`\`
10594
10595---
10596
10597### Issue: False Positive Diffs
10598
10599**Symptom**: Tests show diffs for content that hasn't changed
10600
10601**Common causes**:
106021. Animations not completing
106032. Random IDs or keys
106043. Timestamps
10605
10606**Fixes**:
10607
10608**Animations**: Disable in tests
10609\`\`\`typescript
10610const duration = window.Meticulous?.isRunningAsTest ? 0 : 300
10611\`\`\`
10612
10613**Random IDs**: Use deterministic values
10614\`\`\`typescript
10615const generateId = () => {
10616  if (window.Meticulous?.isRunningAsTest) {
10617    return 'test-id-12345'
10618  }
10619  return crypto.randomUUID()
10620}
10621\`\`\`
10622
10623**Timestamps**: Add \`meticulous-ignore\` class
10624\`\`\`typescript
10625<span className="meticulous-ignore">
10626  {new Date().toLocaleString()}
10627</span>
10628\`\`\`
10629
10630Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
10631
10632---
10633
10634## Testing Best Practices
10635
10636### 1. Test Locally First
10637
10638Run tests locally before CI:
10639
10640\`\`\`bash
10641# Build your app
10642npm run build
10643
10644# Serve built files
10645npx serve dist
10646
10647# In another terminal, run Meticulous
10648npx @alwaysmeticulous/cli simulate \\
10649  --sessionId="YOUR_SESSION_ID" \\
10650  --appUrl="http://localhost:3000"
10651\`\`\`
10652
10653### 2. Handle Loading States
10654
10655Ensure loading states complete:
10656
10657\`\`\`typescript
10658useEffect(() => {
10659  const fetchData = async () => {
10660    setLoading(true)
10661    const data = await getData()
10662    setData(data)
10663    setLoading(false)
10664  }
10665
10666  fetchData()
10667}, [])
10668
10669if (loading) {
10670  return <div>Loading...</div>
10671}
10672\`\`\`
10673
10674### 3. Use Skeleton Screens
10675
10676Instead of spinners:
10677
10678\`\`\`typescript
10679if (loading) {
10680  return <SkeletonCard /> // Consistent placeholder
10681}
10682\`\`\`
10683
10684---
10685
10686## Advanced Configuration
10687
10688### Monorepo Setup
10689
10690If your Vite app is in a subdirectory:
10691
10692\`\`\`yaml
10693- name: Install dependencies
10694  working-directory: ./apps/frontend
10695  run: npm ci
10696
10697- name: Build app
10698  working-directory: ./apps/frontend
10699  run: npm run build
10700
10701- name: Upload and test
10702  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10703  with:
10704    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10705    app-directory: "./apps/frontend/dist"
10706    rewrites: |
10707      [
10708        { "source": "/(.*)", "destination": "/index.html" }
10709      ]
10710\`\`\`
10711
10712### Custom Vite Config
10713
10714If you have a custom Vite config:
10715
10716**File**: \`vite.config.ts\`
10717
10718\`\`\`typescript
10719import { defineConfig } from 'vite'
10720import react from '@vitejs/plugin-react'
10721import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'
10722
10723export default defineConfig({
10724  plugins: [react(), CssSourcemapPlugin()],
10725  build: {
10726    outDir: 'dist',
10727    sourcemap: true,
10728  },
10729  server: {
10730    port: 5173,
10731  }
10732})
10733\`\`\`
10734
10735**Important**: \`build.sourcemap\` covers JavaScript only — it does nothing for CSS. Vite emits no CSS source maps for
10736production builds at all, so without extra help Meticulous cannot attribute stylesheet coverage back to the files in your repo.
10737\`@alwaysmeticulous/recorder-plugin/css-sourcemap\` emits a \`.css.map\` for each CSS asset to close that gap.
10738
10739The plugin disables Vite's CSS minification, which is what makes the maps accurate, so enable it on the build whose coverage
10740Meticulous collects rather than on every production build. See
10741[Viewing source coverage information](${o.ENABLE_SOURCE_COVERAGE_URL}) for the accuracy details and the full set of options.
10742
10743---
10744
10745## See Also
10746
10747- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
10748- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
10749- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
10750`,ta=`---
10751{
10752  "title": "Create React App - Complete Setup Guide"
10753}
10754---
10755
10756# {% $frontmatter.title %}
10757
10758Complete guide for setting up Meticulous with React applications built with Create React App (CRA).
10759
10760---
10761
10762## Overview
10763
10764Create React App is the official way to create single-page React applications. This guide covers:
10765
10766- **Recorder installation** in \`public/index.html\`
10767- **CI/CD configuration** with static asset upload
10768- **Authentication handling**
10769- **Common patterns** and troubleshooting
10770
10771**Prerequisites**:
10772- React application created with Create React App
10773- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
10774
10775**Note**: Create React App is no longer actively maintained. For new projects, consider using [Vite](${o.REACT_VITE_URL}) or Next.js.
10776
10777---
10778
10779## Quick Start
10780
10781### Step 1: Install Recorder in public/index.html
10782
10783Add the Meticulous recorder script to your \`public/index.html\` **before any other scripts**.
10784
10785**File**: \`public/index.html\`
10786
10787\`\`\`html
10788<!DOCTYPE html>
10789<html lang="en">
10790  <head>
10791    <meta charset="utf-8" />
10792    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
10793    <meta name="viewport" content="width=device-width, initial-scale=1" />
10794    <meta name="theme-color" content="#000000" />
10795    <meta name="description" content="Your app description" />
10796    <title>Your App Name</title>
10797
10798    <!-- Meticulous recorder - MUST be first script -->
10799    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
10800    <script
10801      data-project-id="YOUR_PROJECT_ID"
10802      src="https://snippet.meticulous.ai/v1/meticulous.js"
10803    ></script>
10804  </head>
10805  <body>
10806    <noscript>You need to enable JavaScript to run this app.</noscript>
10807    <div id="root"></div>
10808  </body>
10809</html>
10810\`\`\`
10811
10812**Important**: The recorder must load before React to capture all events.
10813
10814---
10815
10816### Step 2: Configure GitHub Actions Workflow
10817
10818CRA builds to static files, so we use the \`upload-assets\` action.
10819
10820**File**: \`.github/workflows/meticulous.yml\`
10821
10822\`\`\`yaml
10823${g}
10824
10825      - uses: actions/setup-node@v4
10826        with:
10827          node-version: 20
10828          cache: 'npm'
10829
10830      - name: Install dependencies
10831        run: npm ci
10832
10833      - name: Build app
10834        run: npm run build
10835        env:
10836          NODE_ENV: production
10837          CI: false # Treats warnings as non-blocking
10838
10839      - name: Upload and test
10840        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
10841        with:
10842          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
10843          app-directory: "build"
10844          rewrites: |
10845            [
10846              { "source": "/(.*)", "destination": "/index.html" }
10847            ]
10848\`\`\`
10849
10850**Note**: \`CI: false\` prevents build from failing on warnings. Remove if you want strict builds.
10851
10852---
10853
10854### Step 3: Add API Token Secret
10855
108561. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
108572. Go to GitHub repo → Settings → Secrets and variables → Actions
108583. Create secret named \`METICULOUS_API_TOKEN\` with your token
10859
10860---
10861
10862## How It Works
10863
10864### upload-assets Action
10865
10866The \`upload-assets\` action:
108671. Uploads your built static files to Meticulous
108682. Serves them on a temporary URL
108693. Runs tests against that URL
108704. Reports diffs back to your PR
10871
10872**Build output**: CRA builds to \`build/\` directory by default.
10873
10874### Rewrites Configuration
10875
10876The \`rewrites\` parameter handles client-side routing:
10877
10878\`\`\`json
10879[
10880  { "source": "/(.*)", "destination": "/index.html" }
10881]
10882\`\`\`
10883
10884This ensures all routes serve \`index.html\`, allowing React Router to handle routing.
10885
10886---
10887
10888## Common Patterns
10889
10890### Pattern 1: Detect Test Mode
10891
10892Use \`window.Meticulous.isRunningAsTest\` in your components:
10893
10894\`\`\`typescript
10895function MyComponent() {
10896  const isTest = window.Meticulous?.isRunningAsTest
10897
10898  if (isTest) {
10899    // Skip animations, use test data, etc.
10900  }
10901
10902  return <div>...</div>
10903}
10904\`\`\`
10905
10906### Pattern 2: Bypass Authentication
10907
10908\`\`\`typescript
10909// In src/App.js or auth provider
10910import { useEffect } from 'react'
10911
10912function App() {
10913  useEffect(() => {
10914    if (window.Meticulous?.isRunningAsTest) {
10915      // Mock authentication for tests
10916      localStorage.setItem('auth-token', 'test-token')
10917      localStorage.setItem('user', JSON.stringify({
10918        id: 'test-user',
10919        name: 'Test User',
10920        email: '[email protected]'
10921      }))
10922    }
10923  }, [])
10924
10925  return <YourApp />
10926}
10927\`\`\`
10928
10929### Pattern 3: Handle Environment Variables
10930
10931CRA uses \`REACT_APP_\` prefix for environment variables:
10932
10933\`\`\`typescript
10934const apiUrl = process.env.REACT_APP_API_URL
10935
10936// Use different URL for tests
10937const effectiveUrl = window.Meticulous?.isRunningAsTest
10938  ? 'https://api.test.example.com'
10939  : apiUrl
10940\`\`\`
10941
10942**In workflow**:
10943\`\`\`yaml
10944- name: Build app
10945  run: npm run build
10946  env:
10947    REACT_APP_API_URL: "https://api.example.com"
10948    NODE_ENV: production
10949    CI: false
10950\`\`\`
10951
10952### Pattern 4: Mock Service Worker for APIs
10953
10954Use MSW to mock API calls:
10955
10956\`\`\`typescript
10957// src/mocks/browser.js
10958import { setupWorker } from 'msw/browser'
10959import { handlers } from './handlers'
10960
10961export const worker = setupWorker(...handlers)
10962
10963// src/index.js
10964if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
10965  const { worker } = await import('./mocks/browser')
10966  await worker.start()
10967}
10968
10969ReactDOM.render(<App />, document.getElementById('root'))
10970\`\`\`
10971
10972---
10973
10974## Complete Example
10975
10976### File Structure
10977
10978\`\`\`
10979your-app/
10980├── public/
10981│   └── index.html            # Recorder installation
10982├── src/
10983│   ├── index.js              # Entry point
10984│   ├── App.js                # Main app component
10985│   ├── components/
10986│   ├── lib/
10987│   │   └── auth.js           # Auth utilities
10988│   └── mocks/                # MSW mocks (optional)
10989├── .github/
10990│   └── workflows/
10991│       └── meticulous.yml    # CI/CD
10992├── package.json
10993└── .env                      # Environment variables
10994\`\`\`
10995
10996### Example: App with Authentication
10997
10998**File**: \`src/App.js\`
10999
11000\`\`\`jsx
11001import { useEffect, useState } from 'react'
11002import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
11003import { checkAuth } from './lib/auth'
11004
11005function App() {
11006  const [user, setUser] = useState(null)
11007  const [loading, setLoading] = useState(true)
11008
11009  useEffect(() => {
11010    // Mock auth for tests
11011    if (window.Meticulous?.isRunningAsTest) {
11012      setUser({
11013        id: 'test-user-123',
11014        name: 'Test User',
11015        email: '[email protected]'
11016      })
11017      setLoading(false)
11018      return
11019    }
11020
11021    // Normal auth flow
11022    checkAuth().then(user => {
11023      setUser(user)
11024      setLoading(false)
11025    }).catch(() => {
11026      setUser(null)
11027      setLoading(false)
11028    })
11029  }, [])
11030
11031  if (loading) {
11032    return (
11033      <div className="loading">
11034        <p>Loading...</p>
11035      </div>
11036    )
11037  }
11038
11039  return (
11040    <BrowserRouter>
11041      <Routes>
11042        <Route path="/" element={<HomePage />} />
11043        <Route
11044          path="/dashboard"
11045          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
11046        />
11047        <Route path="/login" element={<LoginPage />} />
11048      </Routes>
11049    </BrowserRouter>
11050  )
11051}
11052
11053export default App
11054\`\`\`
11055
11056---
11057
11058## CI/CD Configuration Details
11059
11060### Environment Variables
11061
11062CRA environment variables must be prefixed with \`REACT_APP_\`:
11063
11064\`\`\`yaml
11065- name: Build app
11066  run: npm run build
11067  env:
11068    REACT_APP_API_URL: "https://api.example.com"
11069    REACT_APP_APP_NAME: "My App"
11070    NODE_ENV: production
11071    CI: false
11072\`\`\`
11073
11074**In code**:
11075\`\`\`javascript
11076const apiUrl = process.env.REACT_APP_API_URL
11077\`\`\`
11078
11079### Custom Build Script
11080
11081If you have a custom build script:
11082
11083\`\`\`yaml
11084- name: Build app
11085  run: npm run build:custom
11086  env:
11087    NODE_ENV: production
11088\`\`\`
11089
11090### Using Yarn
11091
11092If your project uses Yarn:
11093
11094\`\`\`yaml
11095- uses: actions/setup-node@v4
11096  with:
11097    node-version: 20
11098    cache: 'yarn'
11099
11100- name: Install dependencies
11101  run: yarn install --frozen-lockfile
11102
11103- name: Build app
11104  run: yarn build
11105\`\`\`
11106
11107---
11108
11109## Troubleshooting
11110
11111### Issue: Build Fails with Warnings
11112
11113**Symptom**: Build fails in CI with ESLint or TypeScript warnings
11114
11115**Cause**: CRA treats warnings as errors when \`CI=true\`
11116
11117**Fix**: Set \`CI=false\` in build step:
11118
11119\`\`\`yaml
11120- name: Build app
11121  run: npm run build
11122  env:
11123    CI: false
11124    NODE_ENV: production
11125\`\`\`
11126
11127**Alternative**: Fix the warnings properly
11128
11129---
11130
11131### Issue: Recorder Not Loading
11132
11133**Symptom**: \`window.Meticulous\` is undefined
11134
11135**Checks**:
111361. Verify recorder script is in \`public/index.html\` \`<head>\`
111372. Check project ID is correct
111383. Check for CSP blocking (console errors)
11139
11140**Fix**: Ensure correct placement in \`public/index.html\`:
11141
11142\`\`\`html
11143<head>
11144  <!-- Recorder FIRST -->
11145  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
11146
11147  <!-- Then other head elements -->
11148  <title>Your App</title>
11149</head>
11150\`\`\`
11151
11152---
11153
11154### Issue: Routes Return 404
11155
11156**Symptom**: Direct navigation to routes like \`/about\` returns 404
11157
11158**Cause**: Missing rewrite configuration
11159
11160**Fix**: Ensure rewrites in workflow:
11161
11162\`\`\`yaml
11163rewrites: |
11164  [
11165    { "source": "/(.*)", "destination": "/index.html" }
11166  ]
11167\`\`\`
11168
11169---
11170
11171### Issue: Environment Variables Not Working
11172
11173**Symptom**: \`process.env.REACT_APP_API_URL\` is undefined
11174
11175**Common causes**:
111761. Variable not prefixed with \`REACT_APP_\`
111772. Not added to workflow \`env:\` section
111783. Trying to access in workflow but not in code
11179
11180**Fix**:
11181
11182\`\`\`yaml
11183# In workflow
11184- name: Build app
11185  run: npm run build
11186  env:
11187    REACT_APP_API_URL: "https://api.example.com" # Must have REACT_APP_ prefix
11188\`\`\`
11189
11190\`\`\`javascript
11191// In code
11192const apiUrl = process.env.REACT_APP_API_URL
11193console.log('API URL:', apiUrl) // Should log the URL
11194\`\`\`
11195
11196---
11197
11198### Issue: API Calls Fail
11199
11200**Symptom**: API requests fail during tests
11201
11202**Cause**: No backend in upload-assets mode
11203
11204**Solutions**:
11205
11206**Option 1: Mock APIs with MSW** (recommended — see Pattern 4 above)
11207
11208Keeps you on \`upload-assets\`, which is the simplest and most reliable workflow.
11209
11210**Option 2: Switch to \`upload-container\`** (if you have a backend you want to actually run)
11211
11212Build a Docker image that runs your backend (and optionally your frontend), and switch the workflow to \`upload-container\`:
11213
11214\`\`\`yaml
11215- uses: docker/setup-buildx-action@v3
11216
11217- uses: docker/build-push-action@v6
11218  with:
11219    context: .
11220    tags: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
11221    platforms: linux/amd64
11222    push: false
11223    load: true
11224
11225- name: Run Meticulous tests
11226  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
11227  with:
11228    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11229    image-tag: my-app:\${{ env.METICULOUS_COMMIT_SHA }}
11230    container-port: 3000
11231\`\`\`
11232
11233---
11234
11235### Issue: False Positive Diffs
11236
11237**Symptom**: Tests show diffs for unchanged content
11238
11239**Common causes**:
112401. Animations not completing
112412. Random keys or IDs
112423. Timestamps or dynamic data
11243
11244**Fixes**:
11245
11246**Animations**: Disable in tests
11247\`\`\`javascript
11248const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
11249\`\`\`
11250
11251**Random IDs**: Use deterministic values
11252\`\`\`javascript
11253const generateId = () => {
11254  if (window.Meticulous?.isRunningAsTest) {
11255    return 'test-id-12345'
11256  }
11257  return Math.random().toString(36).substr(2, 9)
11258}
11259\`\`\`
11260
11261**Timestamps**: Add \`meticulous-ignore\` class
11262\`\`\`jsx
11263<span className="meticulous-ignore">
11264  Last updated: {new Date().toLocaleString()}
11265</span>
11266\`\`\`
11267
11268Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
11269
11270---
11271
11272## Testing Best Practices
11273
11274### 1. Test Build Locally
11275
11276Before pushing to CI:
11277
11278\`\`\`bash
11279# Build your app
11280npm run build
11281
11282# Serve built files
11283npx serve -s build
11284
11285# Verify it works at http://localhost:3000
11286\`\`\`
11287
11288### 2. Handle Loading States Properly
11289
11290Ensure loading states complete before rendering content:
11291
11292\`\`\`jsx
11293if (loading) {
11294  return <div className="loading">Loading...</div>
11295}
11296
11297if (error) {
11298  return <div className="error">Error: {error.message}</div>
11299}
11300
11301return <div>{/* Your content */}</div>
11302\`\`\`
11303
11304### 3. Use Consistent Placeholders
11305
11306Instead of spinners, use skeleton screens for consistent layouts:
11307
11308\`\`\`jsx
11309if (loading) {
11310  return <SkeletonCard />
11311}
11312\`\`\`
11313
11314---
11315
11316## Advanced Configuration
11317
11318### Monorepo Setup
11319
11320If your CRA app is in a subdirectory:
11321
11322\`\`\`yaml
11323- name: Install dependencies
11324  working-directory: ./apps/frontend
11325  run: npm ci
11326
11327- name: Build app
11328  working-directory: ./apps/frontend
11329  run: npm run build
11330  env:
11331    CI: false
11332
11333- name: Upload and test
11334  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
11335  with:
11336    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11337    app-directory: "./apps/frontend/build"
11338    rewrites: |
11339      [
11340        { "source": "/(.*)", "destination": "/index.html" }
11341      ]
11342\`\`\`
11343
11344### Custom Public Path
11345
11346If you serve your app from a subdirectory:
11347
11348**In \`package.json\`**:
11349\`\`\`json
11350{
11351  "homepage": "/my-app"
11352}
11353\`\`\`
11354
11355**In workflow**:
11356\`\`\`yaml
11357rewrites: |
11358  [
11359    { "source": "/my-app/(.*)", "destination": "/index.html" }
11360  ]
11361\`\`\`
11362
11363---
11364
11365## Migration to Modern Tools
11366
11367CRA is no longer actively maintained. Consider migrating to:
11368
11369- **Vite**: Faster builds, modern tooling
11370- **Next.js**: Server-side rendering, better performance
11371- **Remix**: Full-stack framework
11372
11373Migration resources:
11374- [Vite Migration Guide](https://vitejs.dev/guide/migration.html)
11375- [Next.js Migration Guide](https://nextjs.org/docs/migrating/from-create-react-app)
11376
11377---
11378
11379## See Also
11380
11381- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
11382- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
11383- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
11384- [React with Vite](${o.REACT_VITE_URL}) - Modern alternative to CRA
11385`,tr=`---
11386{
11387  "title": "Vue 3 with Vite - Complete Setup Guide"
11388}
11389---
11390
11391# {% $frontmatter.title %}
11392
11393Complete guide for setting up Meticulous with Vue 3 applications built with Vite.
11394
11395---
11396
11397## Overview
11398
11399Vue 3 with Vite provides a fast development experience. This guide covers:
11400
11401- **Recorder installation** in \`index.html\`
11402- **CI/CD configuration** with static asset upload
11403- **Authentication handling**
11404- **Common patterns** and troubleshooting
11405
11406**Prerequisites**:
11407- Vue 3 application using Vite
11408- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
11409
11410---
11411
11412## Quick Start
11413
11414### Step 1: Install Recorder in index.html
11415
11416Add the Meticulous recorder script to your \`index.html\` **before any other scripts**.
11417
11418**File**: \`index.html\`
11419
11420\`\`\`html
11421<!DOCTYPE html>
11422<html lang="en">
11423  <head>
11424    <meta charset="UTF-8" />
11425    <link rel="icon" href="/favicon.ico" />
11426    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11427    <title>Your App Name</title>
11428
11429    <!-- Meticulous recorder - MUST be first script -->
11430    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
11431    <script
11432      data-project-id="YOUR_PROJECT_ID"
11433      src="https://snippet.meticulous.ai/v1/meticulous.js"
11434    ></script>
11435  </head>
11436  <body>
11437    <div id="app"></div>
11438    <script type="module" src="/src/main.ts"></script>
11439  </body>
11440</html>
11441\`\`\`
11442
11443**Important**: The recorder must load before your application code.
11444
11445---
11446
11447### Step 2: Configure GitHub Actions Workflow
11448
11449Vue/Vite builds to static files, so we use the \`upload-assets\` action.
11450
11451**File**: \`.github/workflows/meticulous.yml\`
11452
11453\`\`\`yaml
11454${g}
11455
11456      - uses: actions/setup-node@v4
11457        with:
11458          node-version: 20
11459          cache: 'npm'
11460
11461      - name: Install dependencies
11462        run: npm ci
11463
11464      - name: Build app
11465        run: npm run build
11466        env:
11467          NODE_ENV: production
11468
11469      - name: Upload and test
11470        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
11471        with:
11472          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11473          app-directory: "dist"
11474          rewrites: |
11475            [
11476              { "source": "/(.*)", "destination": "/index.html" }
11477            ]
11478\`\`\`
11479
11480---
11481
11482### Step 3: Add API Token Secret
11483
114841. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
114852. Go to GitHub repo → Settings → Secrets and variables → Actions
114863. Create secret named \`METICULOUS_API_TOKEN\` with your token
11487
11488---
11489
11490## How It Works
11491
11492### upload-assets Action
11493
11494The \`upload-assets\` action uploads your built files and runs tests against them.
11495
11496**Build output**: Vite builds to \`dist/\` directory by default.
11497
11498### Rewrites Configuration
11499
11500Handles client-side routing (Vue Router):
11501
11502\`\`\`json
11503[
11504  { "source": "/(.*)", "destination": "/index.html" }
11505]
11506\`\`\`
11507
11508---
11509
11510## Common Patterns
11511
11512### Pattern 1: Detect Test Mode
11513
11514Use \`window.Meticulous.isRunningAsTest\` in your components:
11515
11516**Options API**:
11517\`\`\`vue
11518<template>
11519  <div>
11520    <p v-if="isTest">Running as test</p>
11521    <p v-else>Running normally</p>
11522  </div>
11523</template>
11524
11525<script>
11526export default {
11527  data() {
11528    return {
11529      isTest: window.Meticulous?.isRunningAsTest || false
11530    }
11531  }
11532}
11533</script>
11534\`\`\`
11535
11536**Composition API**:
11537\`\`\`vue
11538<template>
11539  <div>
11540    <p v-if="isTest">Running as test</p>
11541  </div>
11542</template>
11543
11544<script setup>
11545import { ref } from 'vue'
11546
11547const isTest = ref(window.Meticulous?.isRunningAsTest || false)
11548</script>
11549\`\`\`
11550
11551### Pattern 2: Bypass Authentication
11552
11553**File**: \`src/main.ts\`
11554
11555\`\`\`typescript
11556import { createApp } from 'vue'
11557import { createPinia } from 'pinia'
11558import App from './App.vue'
11559import router from './router'
11560
11561const app = createApp(App)
11562
11563// Mock authentication for tests
11564if (window.Meticulous?.isRunningAsTest) {
11565  localStorage.setItem('auth-token', 'test-token')
11566  localStorage.setItem('user', JSON.stringify({
11567    id: 'test-user',
11568    name: 'Test User',
11569    email: '[email protected]'
11570  }))
11571}
11572
11573app.use(createPinia())
11574app.use(router)
11575app.mount('#app')
11576\`\`\`
11577
11578### Pattern 3: Router Navigation Guards
11579
11580Handle authentication in router:
11581
11582**File**: \`src/router/index.ts\`
11583
11584\`\`\`typescript
11585import { createRouter, createWebHistory } from 'vue-router'
11586import type { RouteLocationNormalized } from 'vue-router'
11587
11588const router = createRouter({
11589  history: createWebHistory(import.meta.env.BASE_URL),
11590  routes: [
11591    {
11592      path: '/',
11593      name: 'home',
11594      component: () => import('../views/HomeView.vue')
11595    },
11596    {
11597      path: '/dashboard',
11598      name: 'dashboard',
11599      component: () => import('../views/DashboardView.vue'),
11600      meta: { requiresAuth: true }
11601    }
11602  ]
11603})
11604
11605router.beforeEach((to: RouteLocationNormalized) => {
11606  // Skip auth check during tests
11607  if (window.Meticulous?.isRunningAsTest) {
11608    return true
11609  }
11610
11611  // Normal auth check
11612  if (to.meta.requiresAuth && !isAuthenticated()) {
11613    return { name: 'login', query: { redirect: to.fullPath } }
11614  }
11615
11616  return true
11617})
11618
11619export default router
11620\`\`\`
11621
11622### Pattern 4: Composable for Test Detection
11623
11624Create a reusable composable:
11625
11626**File**: \`src/composables/useMeticulous.ts\`
11627
11628\`\`\`typescript
11629import { ref, readonly } from 'vue'
11630
11631export function useMeticulous() {
11632  const isRunningAsTest = ref(
11633    window.Meticulous?.isRunningAsTest || false
11634  )
11635
11636  const recordCustomValues = (values: Record<string, any>) => {
11637    window.Meticulous?.recordCustomValues?.(values)
11638  }
11639
11640  const getCustomValues = () => {
11641    return window.Meticulous?.getCustomValues?.()
11642  }
11643
11644  return {
11645    isRunningAsTest: readonly(isRunningAsTest),
11646    recordCustomValues,
11647    getCustomValues
11648  }
11649}
11650\`\`\`
11651
11652**Usage**:
11653\`\`\`vue
11654<script setup>
11655import { useMeticulous } from '@/composables/useMeticulous'
11656
11657const { isRunningAsTest } = useMeticulous()
11658</script>
11659\`\`\`
11660
11661---
11662
11663## Complete Example
11664
11665### File Structure
11666
11667\`\`\`
11668your-app/
11669├── src/
11670│   ├── main.ts               # Entry point
11671│   ├── App.vue               # Root component
11672│   ├── router/
11673│   │   └── index.ts          # Router configuration
11674│   ├── stores/               # Pinia stores
11675│   ├── views/                # Page components
11676│   ├── components/           # Reusable components
11677│   └── composables/
11678│       └── useMeticulous.ts  # Test detection composable
11679├── index.html                # Recorder installation
11680├── vite.config.ts            # Vite configuration
11681├── .github/
11682│   └── workflows/
11683│       └── meticulous.yml    # CI/CD
11684└── package.json
11685\`\`\`
11686
11687### Example: Protected View
11688
11689**File**: \`src/views/DashboardView.vue\`
11690
11691\`\`\`vue
11692<template>
11693  <div class="dashboard">
11694    <h1>Welcome, {{ user?.name }}!</h1>
11695    <p>Email: {{ user?.email }}</p>
11696
11697    <div v-if="loading">
11698      <p>Loading dashboard...</p>
11699    </div>
11700    <div v-else-if="error">
11701      <p class="error">{{ error }}</p>
11702    </div>
11703    <div v-else>
11704      <!-- Dashboard content -->
11705    </div>
11706  </div>
11707</template>
11708
11709<script setup lang="ts">
11710import { ref, onMounted } from 'vue'
11711import { useRouter } from 'vue-router'
11712
11713interface User {
11714  id: string
11715  name: string
11716  email: string
11717}
11718
11719const router = useRouter()
11720const user = ref<User | null>(null)
11721const loading = ref(true)
11722const error = ref('')
11723
11724onMounted(async () => {
11725  // Mock data for tests
11726  if (window.Meticulous?.isRunningAsTest) {
11727    user.value = {
11728      id: 'test-user-123',
11729      name: 'Test User',
11730      email: '[email protected]'
11731    }
11732    loading.value = false
11733    return
11734  }
11735
11736  // Normal data fetching
11737  try {
11738    const response = await fetch('/api/user')
11739    if (!response.ok) throw new Error('Failed to fetch user')
11740    user.value = await response.json()
11741  } catch (err) {
11742    error.value = err instanceof Error ? err.message : 'Unknown error'
11743    router.push('/login')
11744  } finally {
11745    loading.value = false
11746  }
11747})
11748</script>
11749\`\`\`
11750
11751---
11752
11753## CI/CD Configuration Details
11754
11755### Environment Variables
11756
11757Vite exposes environment variables prefixed with \`VITE_\`:
11758
11759\`\`\`yaml
11760- name: Build app
11761  run: npm run build
11762  env:
11763    VITE_API_URL: "https://api.example.com"
11764    VITE_APP_NAME: "My App"
11765    NODE_ENV: production
11766\`\`\`
11767
11768**In code**:
11769\`\`\`typescript
11770const apiUrl = import.meta.env.VITE_API_URL
11771\`\`\`
11772
11773### TypeScript Configuration
11774
11775Ensure TypeScript recognizes Vite env variables:
11776
11777**File**: \`src/env.d.ts\`
11778
11779\`\`\`typescript
11780/// <reference types="vite/client" />
11781
11782interface ImportMetaEnv {
11783  readonly VITE_API_URL: string
11784  readonly VITE_APP_NAME: string
11785}
11786
11787interface ImportMeta {
11788  readonly env: ImportMetaEnv
11789}
11790\`\`\`
11791
11792---
11793
11794## Troubleshooting
11795
11796### Issue: Recorder Not Loading
11797
11798**Symptom**: \`window.Meticulous\` is undefined
11799
11800**Checks**:
118011. Verify recorder script in \`index.html\` \`<head>\`
118022. Check project ID is correct
118033. Check for CSP blocking
11804
11805**Fix**: Ensure correct placement:
11806
11807\`\`\`html
11808<head>
11809  <!-- Recorder FIRST -->
11810  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
11811
11812  <!-- Then other elements -->
11813  <title>Your App</title>
11814</head>
11815\`\`\`
11816
11817---
11818
11819### Issue: Routes Return 404
11820
11821**Symptom**: Direct navigation to routes returns 404
11822
11823**Cause**: Missing rewrite configuration
11824
11825**Fix**:
11826
11827\`\`\`yaml
11828rewrites: |
11829  [
11830    { "source": "/(.*)", "destination": "/index.html" }
11831  ]
11832\`\`\`
11833
11834---
11835
11836### Issue: TypeScript Errors
11837
11838**Symptom**: \`Property 'Meticulous' does not exist on type 'Window'.\`
11839
11840**Fix**: Add type declarations
11841
11842**File**: \`src/types/meticulous.d.ts\`
11843
11844\`\`\`typescript
11845interface Meticulous {
11846  isRunningAsTest?: boolean
11847  recordCustomValues?: (values: Record<string, any>) => void
11848  getCustomValues?: () => Record<string, any> | undefined
11849  pause?: () => void
11850  resume?: () => void
11851}
11852
11853declare global {
11854  interface Window {
11855    Meticulous?: Meticulous
11856  }
11857}
11858
11859export {}
11860\`\`\`
11861
11862---
11863
11864### Issue: False Positive Diffs
11865
11866**Common causes**:
118671. Animations not completing
118682. Random data
118693. Timestamps
11870
11871**Fixes**:
11872
11873**Disable animations in tests**:
11874\`\`\`vue
11875<template>
11876  <Transition :duration="transitionDuration">
11877    <div>Content</div>
11878  </Transition>
11879</template>
11880
11881<script setup>
11882import { computed } from 'vue'
11883
11884const transitionDuration = computed(() =>
11885  window.Meticulous?.isRunningAsTest ? 0 : 300
11886)
11887</script>
11888\`\`\`
11889
11890**Deterministic IDs**:
11891\`\`\`typescript
11892const generateId = () => {
11893  if (window.Meticulous?.isRunningAsTest) {
11894    return 'test-id-12345'
11895  }
11896  return crypto.randomUUID()
11897}
11898\`\`\`
11899
11900**Ignore timestamps**:
11901\`\`\`vue
11902<template>
11903  <span class="meticulous-ignore">
11904    {{ new Date().toLocaleString() }}
11905  </span>
11906</template>
11907\`\`\`
11908
11909Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
11910
11911---
11912
11913## Testing Best Practices
11914
11915### 1. Handle Loading States
11916
11917Always show loading states:
11918
11919\`\`\`vue
11920<template>
11921  <div v-if="loading">
11922    <SkeletonLoader />
11923  </div>
11924  <div v-else-if="error">
11925    <ErrorMessage :error="error" />
11926  </div>
11927  <div v-else>
11928    <!-- Content -->
11929  </div>
11930</template>
11931\`\`\`
11932
11933### 2. Use Suspense for Async Components
11934
11935\`\`\`vue
11936<template>
11937  <Suspense>
11938    <template #default>
11939      <AsyncComponent />
11940    </template>
11941    <template #fallback>
11942      <LoadingSpinner />
11943    </template>
11944  </Suspense>
11945</template>
11946\`\`\`
11947
11948### 3. Test Locally First
11949
11950\`\`\`bash
11951# Build your app
11952npm run build
11953
11954# Serve built files
11955npx serve dist
11956
11957# Verify at http://localhost:3000
11958\`\`\`
11959
11960---
11961
11962## Advanced Configuration
11963
11964### Monorepo Setup
11965
11966\`\`\`yaml
11967- name: Install dependencies
11968  working-directory: ./apps/frontend
11969  run: npm ci
11970
11971- name: Build app
11972  working-directory: ./apps/frontend
11973  run: npm run build
11974
11975- name: Upload and test
11976  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
11977  with:
11978    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
11979    app-directory: "./apps/frontend/dist"
11980    rewrites: |
11981      [
11982        { "source": "/(.*)", "destination": "/index.html" }
11983      ]
11984\`\`\`
11985
11986### Custom Vite Config
11987
11988**File**: \`vite.config.ts\`
11989
11990\`\`\`typescript
11991import { fileURLToPath, URL } from 'node:url'
11992import { defineConfig } from 'vite'
11993import vue from '@vitejs/plugin-vue'
11994import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'
11995
11996export default defineConfig({
11997  plugins: [vue(), CssSourcemapPlugin()],
11998  resolve: {
11999    alias: {
12000      '@': fileURLToPath(new URL('./src', import.meta.url))
12001    }
12002  },
12003  build: {
12004    outDir: 'dist',
12005    sourcemap: true
12006  }
12007})
12008\`\`\`
12009
12010**Important**: \`build.sourcemap\` covers JavaScript only — it does nothing for CSS. Vite emits no CSS source maps for
12011production builds at all, so without extra help Meticulous cannot attribute stylesheet coverage back to the files in your repo.
12012\`@alwaysmeticulous/recorder-plugin/css-sourcemap\` emits a \`.css.map\` for each CSS asset to close that gap.
12013
12014The plugin disables Vite's CSS minification, which is what makes the maps accurate, so enable it on the build whose coverage
12015Meticulous collects rather than on every production build. See
12016[Viewing source coverage information](${o.ENABLE_SOURCE_COVERAGE_URL}) for the accuracy details and the full set of options.
12017
12018---
12019
12020## See Also
12021
12022- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}) - General Meticulous setup
12023- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
12024- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
12025`,tl=`---
12026{
12027  "title": "Angular - Complete Setup Guide"
12028}
12029---
12030
12031# {% $frontmatter.title %}
12032
12033Complete guide for setting up Meticulous with Angular applications.
12034
12035---
12036
12037## Overview
12038
12039Angular is a comprehensive framework for building web applications. This guide covers:
12040
12041- **Recorder installation** in \`src/index.html\`
12042- **CI/CD configuration** with static asset upload
12043- **Authentication handling**
12044- **Common patterns** and troubleshooting
12045
12046**Prerequisites**:
12047- Angular application (version 12+)
12048- Basic familiarity with [Meticulous concepts](${o.ONBOARDING_GUIDE_URL})
12049
12050---
12051
12052## Quick Start
12053
12054### Step 1: Install Recorder in src/index.html
12055
12056Add the Meticulous recorder script to your \`src/index.html\` **before any other scripts**.
12057
12058**File**: \`src/index.html\`
12059
12060\`\`\`html
12061<!doctype html>
12062<html lang="en">
12063<head>
12064  <meta charset="utf-8">
12065  <title>Your App Name</title>
12066  <base href="/">
12067  <meta name="viewport" content="width=device-width, initial-scale=1">
12068  <link rel="icon" type="image/x-icon" href="favicon.ico">
12069
12070  <!-- Meticulous recorder - MUST be first script -->
12071  <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
12072  <script
12073    data-project-id="YOUR_PROJECT_ID"
12074    src="https://snippet.meticulous.ai/v1/meticulous.js"
12075  ></script>
12076</head>
12077<body>
12078  <app-root></app-root>
12079</body>
12080</html>
12081\`\`\`
12082
12083**Important**: The recorder must load before Angular bootstraps.
12084
12085---
12086
12087### Step 2: Configure GitHub Actions Workflow
12088
12089Angular builds to static files, so we use the \`upload-assets\` action.
12090
12091**File**: \`.github/workflows/meticulous.yml\`
12092
12093\`\`\`yaml
12094${g}
12095
12096      - uses: actions/setup-node@v4
12097        with:
12098          node-version: 20
12099          cache: 'npm'
12100
12101      - name: Install dependencies
12102        run: npm ci
12103
12104      - name: Build app
12105        run: npm run build
12106        env:
12107          NODE_ENV: production
12108
12109      - name: Upload and test
12110        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
12111        with:
12112          api-token: \${{ secrets.METICULOUS_API_TOKEN }}
12113          app-directory: "dist/your-app-name"
12114          rewrites: |
12115            [
12116              { "source": "/(.*)", "destination": "/index.html" }
12117            ]
12118\`\`\`
12119
12120**Important**: Replace \`your-app-name\` with your actual app name from \`angular.json\`.
12121
12122---
12123
12124### Step 3: Add API Token Secret
12125
121261. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
121272. Go to GitHub repo → Settings → Secrets and variables → Actions
121283. Create secret named \`METICULOUS_API_TOKEN\` with your token
12129
12130---
12131
12132## Finding Your App Name
12133
12134The build output directory depends on your app name in \`angular.json\`:
12135
12136**File**: \`angular.json\`
12137
12138\`\`\`json
12139{
12140  "projects": {
12141    "my-angular-app": {
12142      "architect": {
12143        "build": {
12144          "options": {
12145            "outputPath": "dist/my-angular-app"
12146          }
12147        }
12148      }
12149    }
12150  }
12151}
12152\`\`\`
12153
12154Use \`dist/my-angular-app\` as the \`app-directory\` in your workflow.
12155
12156---
12157
12158## Common Patterns
12159
12160### Pattern 1: Detect Test Mode
12161
12162For 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.
12163
12164**Component**:
12165\`\`\`typescript
12166import { Component, OnInit } from '@angular/core'
12167
12168@Component({
12169  selector: 'app-dashboard',
12170  templateUrl: './dashboard.component.html'
12171})
12172export class DashboardComponent implements OnInit {
12173  isTest = false
12174
12175  ngOnInit() {
12176    this.isTest = (window as any).Meticulous?.isRunningAsTest || false
12177
12178    if (this.isTest) {
12179      // Skip animations, use test data, etc.
12180    }
12181  }
12182}
12183\`\`\`
12184
12185**Template**:
12186\`\`\`html
12187<div *ngIf="isTest" class="test-indicator">
12188  Running as test
12189</div>
12190\`\`\`
12191
12192### Pattern 2: Bypass Authentication
12193
12194**File**: \`src/app/app.component.ts\`
12195
12196\`\`\`typescript
12197import { Component, OnInit } from '@angular/core'
12198import { Router } from '@angular/router'
12199
12200@Component({
12201  selector: 'app-root',
12202  templateUrl: './app.component.html'
12203})
12204export class AppComponent implements OnInit {
12205  constructor(private router: Router) {}
12206
12207  ngOnInit() {
12208    // Mock authentication for tests
12209    if ((window as any).Meticulous?.isRunningAsTest) {
12210      localStorage.setItem('auth-token', 'test-token')
12211      localStorage.setItem('user', JSON.stringify({
12212        id: 'test-user',
12213        name: 'Test User',
12214        email: '[email protected]'
12215      }))
12216    }
12217  }
12218}
12219\`\`\`
12220
12221### Pattern 3: Route Guards
12222
12223Handle authentication in route guards:
12224
12225**File**: \`src/app/guards/auth.guard.ts\`
12226
12227\`\`\`typescript
12228import { Injectable } from '@angular/core'
12229import { Router, CanActivate } from '@angular/router'
12230import { AuthService } from '../services/auth.service'
12231
12232@Injectable({
12233  providedIn: 'root'
12234})
12235export class AuthGuard implements CanActivate {
12236  constructor(
12237    private authService: AuthService,
12238    private router: Router
12239  ) {}
12240
12241  canActivate(): boolean {
12242    // Skip auth check during tests
12243    if ((window as any).Meticulous?.isRunningAsTest) {
12244      return true
12245    }
12246
12247    // Normal auth check
12248    if (this.authService.isAuthenticated()) {
12249      return true
12250    }
12251
12252    this.router.navigate(['/login'])
12253    return false
12254  }
12255}
12256\`\`\`
12257
12258### Pattern 4: Service for Test Detection
12259
12260Create a reusable service:
12261
12262**File**: \`src/app/services/meticulous.service.ts\`
12263
12264\`\`\`typescript
12265import { Injectable } from '@angular/core'
12266
12267interface MeticulousWindow extends Window {
12268  Meticulous?: {
12269    isRunningAsTest?: boolean
12270    recordCustomValues?: (values: Record<string, any>) => void
12271    getCustomValues?: () => Record<string, any> | undefined
12272  }
12273}
12274
12275@Injectable({
12276  providedIn: 'root'
12277})
12278export class MeticulousService {
12279  get isRunningAsTest(): boolean {
12280    return (window as MeticulousWindow).Meticulous?.isRunningAsTest || false
12281  }
12282
12283  recordCustomValues(values: Record<string, any>): void {
12284    (window as MeticulousWindow).Meticulous?.recordCustomValues?.(values)
12285  }
12286
12287  getCustomValues(): Record<string, any> | undefined {
12288    return (window as MeticulousWindow).Meticulous?.getCustomValues?.()
12289  }
12290}
12291\`\`\`
12292
12293**Usage**:
12294\`\`\`typescript
12295import { Component, OnInit } from '@angular/core'
12296import { MeticulousService } from './services/meticulous.service'
12297
12298@Component({
12299  selector: 'app-my-component',
12300  templateUrl: './my-component.component.html'
12301})
12302export class MyComponent implements OnInit {
12303  constructor(private meticulous: MeticulousService) {}
12304
12305  ngOnInit() {
12306    if (this.meticulous.isRunningAsTest) {
12307      // Test-specific logic
12308    }
12309  }
12310}
12311\`\`\`
12312
12313---
12314
12315## Complete Example
12316
12317### File Structure
12318
12319\`\`\`
12320your-app/
12321├── src/
12322│   ├── app/
12323│   │   ├── app.component.ts      # Root component
12324│   │   ├── app-routing.module.ts # Router configuration
12325│   │   ├── guards/
12326│   │   │   └── auth.guard.ts     # Auth guard
12327│   │   ├── services/
12328│   │   │   ├── auth.service.ts   # Auth service
12329│   │   │   └── meticulous.service.ts
12330│   │   └── components/
12331│   ├── index.html                # Recorder installation
12332│   └── main.ts                   # Bootstrap
12333├── angular.json                  # Angular configuration
12334├── .github/
12335│   └── workflows/
12336│       └── meticulous.yml        # CI/CD
12337└── package.json
12338\`\`\`
12339
12340### Example: Protected Component
12341
12342**File**: \`src/app/components/dashboard/dashboard.component.ts\`
12343
12344\`\`\`typescript
12345import { Component, OnInit } from '@angular/core'
12346import { Router } from '@angular/router'
12347import { MeticulousService } from '../../services/meticulous.service'
12348
12349interface User {
12350  id: string
12351  name: string
12352  email: string
12353}
12354
12355@Component({
12356  selector: 'app-dashboard',
12357  templateUrl: './dashboard.component.html',
12358  styleUrls: ['./dashboard.component.css']
12359})
12360export class DashboardComponent implements OnInit {
12361  user: User | null = null
12362  loading = true
12363  error = ''
12364
12365  constructor(
12366    private router: Router,
12367    private meticulous: MeticulousService
12368  ) {}
12369
12370  async ngOnInit() {
12371    // Mock data for tests
12372    if (this.meticulous.isRunningAsTest) {
12373      this.user = {
12374        id: 'test-user-123',
12375        name: 'Test User',
12376        email: '[email protected]'
12377      }
12378      this.loading = false
12379      return
12380    }
12381
12382    // Normal data fetching
12383    try {
12384      const response = await fetch('/api/user')
12385      if (!response.ok) throw new Error('Failed to fetch user')
12386      this.user = await response.json()
12387    } catch (err) {
12388      this.error = err instanceof Error ? err.message : 'Unknown error'
12389      this.router.navigate(['/login'])
12390    } finally {
12391      this.loading = false
12392    }
12393  }
12394}
12395\`\`\`
12396
12397**File**: \`src/app/components/dashboard/dashboard.component.html\`
12398
12399\`\`\`html
12400<div class="dashboard">
12401  <h1 *ngIf="user">Welcome, {{ user.name }}!</h1>
12402
12403  <div *ngIf="loading">
12404    <p>Loading dashboard...</p>
12405  </div>
12406
12407  <div *ngIf="error" class="error">
12408    <p>{{ error }}</p>
12409  </div>
12410
12411  <div *ngIf="!loading && !error && user">
12412    <p>Email: {{ user.email }}</p>
12413    <!-- Dashboard content -->
12414  </div>
12415</div>
12416\`\`\`
12417
12418---
12419
12420## CI/CD Configuration Details
12421
12422### Environment Variables
12423
12424Angular doesn't have built-in support for runtime environment variables in the browser. Use build-time configuration:
12425
12426**File**: \`src/environments/environment.prod.ts\`
12427
12428\`\`\`typescript
12429export const environment = {
12430  production: true,
12431  apiUrl: 'https://api.example.com'
12432}
12433\`\`\`
12434
12435**In workflow**:
12436\`\`\`yaml
12437- name: Build app
12438  run: npm run build -- --configuration production
12439\`\`\`
12440
12441### Custom Output Directory
12442
12443If you have a custom output directory in \`angular.json\`:
12444
12445\`\`\`json
12446{
12447  "architect": {
12448    "build": {
12449      "options": {
12450        "outputPath": "build"
12451      }
12452    }
12453  }
12454}
12455\`\`\`
12456
12457Update workflow:
12458\`\`\`yaml
12459- name: Upload and test
12460  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
12461  with:
12462    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
12463    app-directory: "build"
12464\`\`\`
12465
12466---
12467
12468## Troubleshooting
12469
12470### Issue: Recorder Not Loading
12471
12472**Symptom**: \`window.Meticulous\` is undefined
12473
12474**Checks**:
124751. Verify recorder script in \`src/index.html\` \`<head>\`
124762. Check project ID is correct
124773. Check for CSP blocking
12478
12479**Fix**: Ensure correct placement:
12480
12481\`\`\`html
12482<head>
12483  <!-- Recorder FIRST -->
12484  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
12485
12486  <!-- Then other elements -->
12487  <meta name="viewport" content="width=device-width, initial-scale=1">
12488</head>
12489\`\`\`
12490
12491---
12492
12493### Issue: TypeScript Errors
12494
12495**Symptom**: \`Property 'Meticulous' does not exist on type 'Window'.\`
12496
12497**Fix**: Add type declarations
12498
12499**File**: \`src/typings.d.ts\`
12500
12501\`\`\`typescript
12502interface Meticulous {
12503  isRunningAsTest?: boolean
12504  recordCustomValues?: (values: Record<string, any>) => void
12505  getCustomValues?: () => Record<string, any> | undefined
12506  pause?: () => void
12507  resume?: () => void
12508}
12509
12510declare interface Window {
12511  Meticulous?: Meticulous
12512}
12513\`\`\`
12514
12515**Update**: \`tsconfig.app.json\`
12516
12517\`\`\`json
12518{
12519  "files": [
12520    "src/main.ts",
12521    "src/typings.d.ts"
12522  ]
12523}
12524\`\`\`
12525
12526---
12527
12528### Issue: Wrong Output Directory
12529
12530**Symptom**: \`app-directory "dist/your-app-name" not found\`
12531
12532**Cause**: App name doesn't match \`angular.json\`
12533
12534**Fix**: Check \`angular.json\` for correct output path:
12535
12536\`\`\`bash
12537# Find your app name
12538cat angular.json | grep "outputPath"
12539
12540# Output example: "outputPath": "dist/my-app"
12541# Use "dist/my-app" in workflow
12542\`\`\`
12543
12544---
12545
12546### Issue: Routes Return 404
12547
12548**Symptom**: Direct navigation to routes returns 404
12549
12550**Cause**: Missing rewrite configuration
12551
12552**Fix**:
12553
12554\`\`\`yaml
12555rewrites: |
12556  [
12557    { "source": "/(.*)", "destination": "/index.html" }
12558  ]
12559\`\`\`
12560
12561---
12562
12563### Issue: False Positive Diffs
12564
12565**Common causes**:
125661. Animations not completing
125672. Random data
125683. Timestamps
12569
12570**Fixes**:
12571
12572**Disable animations in tests**:
12573\`\`\`typescript
12574import { Component, OnInit } from '@angular/core'
12575import { MeticulousService } from './services/meticulous.service'
12576
12577@Component({
12578  selector: 'app-my-component',
12579  animations: [/* your animations */]
12580})
12581export class MyComponent implements OnInit {
12582  animationState = 'initial'
12583
12584  constructor(private meticulous: MeticulousService) {}
12585
12586  ngOnInit() {
12587    // Skip animations in tests
12588    if (this.meticulous.isRunningAsTest) {
12589      this.animationState = 'final'
12590    }
12591  }
12592}
12593\`\`\`
12594
12595**Deterministic values**:
12596\`\`\`typescript
12597generateId(): string {
12598  if ((window as any).Meticulous?.isRunningAsTest) {
12599    return 'test-id-12345'
12600  }
12601  return crypto.randomUUID()
12602}
12603\`\`\`
12604
12605**Ignore timestamps**:
12606\`\`\`html
12607<span class="meticulous-ignore">
12608  {{ currentDate | date:'medium' }}
12609</span>
12610\`\`\`
12611
12612Learn more: [Fix False Positive Diffs](${o.FIX_FALSE_POSITIVES_URL})
12613
12614---
12615
12616## Testing Best Practices
12617
12618### 1. Handle Loading States
12619
12620Always show loading states:
12621
12622\`\`\`html
12623<div *ngIf="loading">
12624  <app-skeleton-loader></app-skeleton-loader>
12625</div>
12626<div *ngIf="!loading && !error">
12627  <!-- Content -->
12628</div>
12629<div *ngIf="error">
12630  <app-error-message [error]="error"></app-error-message>
12631</div>
12632\`\`\`
12633
12634### 2. Use Resolvers for Data
12635
12636Angular resolvers ensure data is loaded before navigation:
12637
12638\`\`\`typescript
12639import { Injectable } from '@angular/core'
12640import { Resolve } from '@angular/router'
12641import { Observable } from 'rxjs'
12642
12643@Injectable({
12644  providedIn: 'root'
12645})
12646export class UserResolver implements Resolve<User> {
12647  constructor(
12648    private userService: UserService,
12649    private meticulous: MeticulousService
12650  ) {}
12651
12652  resolve(): Observable<User> | Promise<User> | User {
12653    if (this.meticulous.isRunningAsTest) {
12654      return {
12655        id: 'test-user',
12656        name: 'Test User',
12657        email: '[email protected]'
12658      }
12659    }
12660
12661    return this.userService.getUser()
12662  }
12663}
12664\`\`\`
12665
12666### 3. Test Locally First
12667
12668\`\`\`bash
12669# Build your app
12670npm run build
12671
12672# Serve built files
12673npx http-server dist/your-app-name
12674
12675# Verify at http://localhost:8080
12676\`\`\`
12677
12678---
12679
12680## Advanced Configuration
12681
12682### Monorepo Setup
12683
12684\`\`\`yaml
12685- name: Install dependencies
12686  working-directory: ./apps/frontend
12687  run: npm ci
12688
12689- name: Build app
12690  working-directory: ./apps/frontend
12691  run: npm run build
12692
12693- name: Upload and test
12694  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
12695  with:
12696    api-token: \${{ secrets.METICULOUS_API_TOKEN }}
12697    app-directory: "./apps/frontend/dist/frontend"
12698    rewrites: |
12699      [
12700        { "source": "/(.*)", "destination": "/index.html" }
12701      ]
12702\`\`\`
12703
12704### Base Href Configuration
12705
12706If your app is served from a subdirectory:
12707
12708**In \`angular.json\`**:
12709\`\`\`json
12710{
12711  "build": {
12712    "options": {
12713      "baseHref": "/my-app/"
12714    }
12715  }
12716}
12717\`\`\`
12718
12719**In workflow**:
12720\`\`\`yaml
12721rewrites: |
12722  [
12723    { "source": "/my-app/(.*)", "destination": "/index.html" }
12724  ]
12725\`\`\`
12726
12727---
12728
12729## See Also
12730
12731- [Onboarding Guide](${o.ONBOARDING_GUIDE_URL}
12731) - General Meticulous setup
12732- [Troubleshoot Authentication](${o.TROUBLESHOOT_AUTH_URL}) - Auth patterns and solutions
12733- [Fix False Positives](${o.FIX_FALSE_POSITIVES_URL}) - Handle non-deterministic content
12734`,tc=`---
12735{
12736  "title": "Trigger the Meticulous tests by manually creating deployments on GitHub"
12737}
12738---
12739
12740# {% $frontmatter.title %}
12741
12742If 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
12743the Meticulous tests by manually creating deployments on GitHub. By tagging commits in your repository with a link to the deployment URL Meticulous
12744can then automatically run the tests for that commit, and compare the results of the tests between commits to your feature branches and the corresponding
12745base commit on the main/master branch.
12746
12747If 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
12748Meticulous will work out of the box.
12749
12750### How to manually create deployments on GitHub
12751
12752To begin with:
12753
12754 1. Make sure you have the [Meticulous GitHub app](${i.METICULOUS_GITHUB_APP_INSTALL_URL}) installed.
12755 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.
12756
12757For 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:
12758
12759  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).
12760  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'.
12761  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.
12762  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.
12763
12764Once 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.
12765
12766Meticulous 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.
12767
12768${i.WHERE_CAN_I_REACH_OUT_FOR_SUPPORT}
12769`;var tu=e.i(458636),td=e.i(991234);let th=[{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:`---
12770{
12771  "title": "Require approving diffs before merging a PR"
12772}
12773---
12774
12775# {% $frontmatter.title %}
12776
12777{% tabs %}
12778{% tab label="GitHub" %}
12779
12780If you've installed the [Meticulous GitHub App](https://github.com/apps/alwaysmeticulous) Meticulous will add a check on your PR that is red
12781if there are diffs that haven't been approved yet and becomes green once you click the green 'Approve all Visual Differences' button.
12782This button can be found by clicking on the link in the Meticulous comment.
12783
12784If 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.
12785
12786To do so click on the *'Settings'* tab of your repo, and then select the *'Branches'* tab:
12787
12788![Meticulous comment](https://assets.meticulous.ai/docs/github-actions-branch-protection-rules.png)
12789
12790Select 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:
12791
12792![Meticulous comment](https://assets.meticulous.ai/docs/github-actions-branch-protection-rules-main-branch.png)
12793
12794{% callout type="warning" %}
12795**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.
12796{% /callout %}
12797
12798{% /tab %}
12799{% tab label="GitLab" %}
12800
12801Requiring diffs to be approved before merging is not yet supported on GitLab.
12802
12803{% /tab %}
12804{% /tabs %}
12805`},{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}
12805,{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:`---
12806{
12807  "title": "TypeScript Types for window.Meticulous"
12808}
12809---
12810
12811# {% $frontmatter.title %}
12812
12813TypeScript definitions for the \`window.Meticulous\` object are available in the [\`@alwaysmeticulous/sdk-bundles-api\`](https://www.npmjs.com/package/@alwaysmeticulous/sdk-bundles-api) package.
12814
12815## Installation
12816
12817Install the package as a dev dependency:
12818
12819\`\`\`bash
12820npm install --save-dev @alwaysmeticulous/sdk-bundles-api@latest
12821\`\`\`
12822
12823## Usage
12824
12825Import the type and extend the Window interface:
12826
12827\`\`\`typescript
12828import type { MeticulousPublicApi } from '@alwaysmeticulous/sdk-bundles-api';
12829
12830declare global {
12831  interface Window {
12832    Meticulous?: MeticulousPublicApi;
12833  }
12834}
12835\`\`\`
12836
12837Now you have full type safety when using the \`window.Meticulous\` object:
12838
12839\`\`\`typescript
12840// Detect if running as a test
12841if (window.Meticulous?.isRunningAsTest) {
12842  console.log('Running as a Meticulous test');
12843}
12844
12845// Record session context with type safety
12846window.Meticulous?.context.recordUserId('user-123');
12847window.Meticulous?.context.recordUserEmail('[email protected]');
12848window.Meticulous?.context.recordFeatureFlag('myFlag', true);
12849window.Meticulous?.context.recordCustomContext('userRole', 'admin');
12850
12851const override = window.Meticulous?.context?.getFlagOverride?.('myFlag');
12852if (override?.overridden) {
12853  // Prefer override.value in your resolver, then record that same value
12854}
12855\`\`\`
12856
12857## Full API Reference
12858
12859For 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.
12860`},{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:ev},{id:"record-custom-values",url:"/docs/how-to/record-custom-values",document:ek},{id:"record-session-context",url:"/docs/how-to/record-session-context",document:eS},{id:"ignore-url-patterns",url:"/docs/how-to/ignore-url-patterns",document:`---
12861{
12862  "title": "Ignoring URL patterns"
12863}
12864---
12865
12866# {% $frontmatter.title %}
12867
12868You 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.
12869
12870### Usage
12871
12872To ignore URL patterns, set the \`window.METICULOUS_IGNORE_URL_PATTERNS\` array before loading the recorder script.
12873
12874#### Example
12875
12876\`\`\`html
12877<script>
12878  window.METICULOUS_IGNORE_URL_PATTERNS = [
12879    "^https://.*\\.amazonaws\\.com/.*"
12880  ];
12881</script>
12882
12883<!-- Load the recorder script after setting the ignore patterns -->
12884<script
12885  data-recording-token="<YOUR_RECORDING_TOKEN>"
12886  data-is-production-environment="false"
12887  src="https://snippet.meticulous.ai/v1/meticulous.js">
12888</script>
12889\`\`\`
12890
12891### Syntax
12892
12893The patterns are defined as strings and are treated as standard JavaScript regular expressions. They are matched against the full URL using \`String.match()\`.
12894
12895### Why no allowlist?
12896
12897We 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.
12898`},{id:"troubleshoot-auth",url:"/docs/how-to/troubleshoot-auth",document:eT},{id:"troubleshoot-recorder",url:"/docs/how-to/troubleshoot-recorder",document:e_},{id:"troubleshoot-replay-accuracy",url:"/docs/how-to/troubleshoot-replay-accuracy",document:eI},{id:"troubleshoot-failed-simulations",url:"/docs/how-to/troubleshoot-failed-simulations",document:eR},{id:"ensure-recorder-captures-all-requests",url:"/docs/how-to/ensure-recorder-captures-all-requests",document:eC},{id:"enable-source-coverage",url:"/docs/how-to/enable-source-coverage",document:ex},{id:"blocked-requests",url:"/docs/how-to/blocked-requests",document:eA},{id:"configure-ignore-patterns",url:"/docs/how-to/configure-ignore-patterns",document:eM},{id:"built-in-checks",url:"/docs/built-in-checks",document:eq},{id:"built-in-checks-accessibility",url:"/docs/built-in-checks/accessibility",document:eW},{id:"built-in-checks-network-requests",url:"/docs/built-in-checks/network-requests",document:eV},{id:"built-in-checks-react-component-renders",url:"/docs/built-in-checks/react-component-renders",document:eJ}
12898,{id:"custom-checks",url:"/docs/custom-checks",document:eD},{id:"custom-checks-writing-a-custom-check",url:"/docs/custom-checks/writing-a-custom-check",document:ej},{id:"custom-checks-recording-custom-data",url:"/docs/custom-checks/recording-custom-data",document:eF},{id:"custom-checks-built-in-snapshot-types",url:"/docs/custom-checks/built-in-snapshot-types",document:eH},{id:"custom-checks-best-practices",url:"/docs/custom-checks/best-practices",document:e$},{id:"handle-file-uploads",url:"/docs/how-to/handle-file-uploads",document:eE},{id:"companion-assets-advanced",url:"/docs/how-to/companion-assets-advanced",document:eU},{id:"incremental-asset-upload",url:"/docs/how-to/incremental-asset-upload",document:`---
12899{
12900  "title": "Incremental Asset Upload"
12901}
12902---
12903
12904# {% $frontmatter.title %}
12905
12906{% callout type="info" title="This is an advanced workflow" %}
12907Most 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.
12908{% /callout %}
12909
12910## Overview
12911
12912With incremental asset upload, you split your build into named **chunks** and upload each chunk to Meticulous independently. A chunk that
12913hasn'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
12914modified in the build has been uploaded, you trigger a test run by pointing Meticulous at a **manifest** that specifies the version to use
12915for each chunk, so that Meticulous can re-assemble a full build.
12916
12917This splits the work into two commands:
12918
129191. [\`ci upload-asset-chunk\`](#1-upload-each-chunk) — upload a single chunk (skipped instantly if already uploaded).
129202. [\`ci run-with-uploaded-asset-chunks\`](#2-trigger-the-test-run) — assemble the referenced chunks and trigger a test run.
12921
12922Meticulous 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.
12923
12924---
12925
12926## How Chunks Are Assembled
12927
12928Each chunk has:
12929
12930- **\`chunkName\`** — a logical name for the chunk (e.g. \`app\`, \`vendor\`, \`home-page-app\`). Chunks are deduped by the \`(chunkName, chunkVersionId)\` pair.
12931- **\`chunkVersionId\`** — a version identifier for the chunk's contents. See [Choosing a version id](#choosing-a-version-id).
12932- **\`chunkAssetsDirectory\`** — the local directory whose contents are packaged into the chunk.
12933- **\`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.
12934
12935When Meticulous assembles a build, it lays each chunk's files down under its prefix. For example, given:
12936
12937- **chunk1** — prefix \`""\`, contains \`file1.js\` and \`sub-folder/file2.js\`
12938- **chunk2** — prefix \`""\`, contains \`file2.js\`
12939- **chunk3** — prefix \`"sub-folder2"\`, contains \`file3.js\`
12940
12941the assembled directory is:
12942
12943\`\`\`
12944file1.js
12945file2.js
12946sub-folder/file2.js
12947sub-folder2/file3.js
12948\`\`\`
12949
12950{% callout type="warning" title="Colliding paths resolve last-wins" %}
12951If 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.
12952{% /callout %}
12953
12954### Choosing a version id
12955
12956It 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.
12957
12958It 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.
12959
12960A version id can be:
12961
12962- A hash of the chunk's contents (e.g. the directory's content hash), or
12963- A hash of the inputs to the build task that produced the chunk (e.g. the build cache key).
12964
12965You'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
12966in 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
12967well, 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
12968contents of the chunk (hash of the output).
12969
12970---
12971
12972## 1. Upload Each Chunk
12973
12974For every chunk that makes up your build, call \`ci upload-asset-chunk\`:
12975
12976\`\`\`bash
12977npx @alwaysmeticulous/cli ci upload-asset-chunk \\
12978  --apiToken="$METICULOUS_API_TOKEN" \\
12979  --chunkName="home-page-app" \\
12980  --chunkVersionId="ad8a8da9aaaweaad9" \\
12981  --chunkAssetsDirectory="dist/home-page-app" \\
12982  --chunkAssetsDirectoryPrefix="" \\
12983  --commitSha="$CI_COMMIT_SHA"
12984\`\`\`
12985
12986If a chunk with the same \`chunkName\` and \`chunkVersionId\` is already uploaded, the command exits \`0\` after compressing but before uploading.
12987
12988For the first build on your main branch you run you'll need to upload **every** chunk in your application. If you consistently run builds
12989for every commit in the chain from thereon then you'll only need to upload the chunks that changed in that commit -- *assuming
12990that every ancestor commit successfully uploaded all of its changed chunks, all the way back to a commit where you uploaded every chunk*.
12991We therefore recommend either calling \`upload-asset-chunk\` for every commit, or using your build cache to skip chunk versions which you know
12992for certain have already been uploaded, since there is already a cache entry from that build task.
12993
12994For more information on the command run \`npx @alwaysmeticulous/cli ci upload-asset-chunk\`, or
12995[view the source code](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/cli/src/commands/ci/upload-asset-chunk.command.ts
12995).
12996
12997---
12998
12999## 2. Trigger the Test Run
13000
13001Once 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\`.
13002
13003The manifest is a JSON array of \`{ name, versionId }\` references:
13004
13005\`\`\`bash
13006cat > assets-manifest.json <<'EOF'
13007[
13008  { "name": "home-page-app", "versionId": "ad8a8da9aaaweaad9" },
13009  { "name": "settings-page-app",   "versionId": "dd8ffdaa9dfedebb3" }
13010]
13011EOF
13012
13013npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
13014  --apiToken="$METICULOUS_API_TOKEN" \\
13015  --assetReferencesManifest="./assets-manifest.json" \\
13016  --commitSha="$CI_COMMIT_SHA" \\
13017  --waitForBase
13018\`\`\`
13019
13020Because 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.
13021
13022This 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.
13023
13024To restrict the run to sessions starting on specific routes, pass \`--sessionFilter\` — see
13025[Filter Sessions by Start URL](/docs/how-to/filter-sessions-by-start-url).
13026
13027For more information on the command run \`npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks\`, or
13028[view the source code](https://github.com/alwaysmeticulous/meticulous-sdk/blob/main/packages/cli/src/commands/ci/run-with-uploaded-asset-chunks.command.ts).
13029
13030### Manifest format
13031
13032The manifest is a non-empty JSON array with no duplicate chunk names. Each entry must be an object in one of two shapes:
13033
13034- \`{ "name": string, "versionId": string }\` — an explicit chunk version (both values non-empty), or
13035- \`{ "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)).
13036
13037\`\`\`json
13038[
13039  { "name": "charts-app", "versionId": "ad8a8da9aaaweaad9" },
13040  { "name": "plugin-1",   "versionLookup": "latest-in-history" }
13041]
13042\`\`\`
13043
13044Names should be as stable as possible across builds.
13045
13046### Version lookup for unchanged chunks
13047
13048Instead of computing and tracking version ids for chunks that haven't changed, you can reference them with
13049\`{ "name": "<chunk>", "versionLookup": "latest-in-history" }\`. When the test run is triggered, Meticulous walks the base test run and its
13050ancestors (up to 16 levels) and resolves the lookup to the version that chunk had in the **nearest ancestor test run** whose manifest
13051included it. Chunks with explicit \`versionId\`s are used as-is.
13052
13053This means your CI only needs to compute version ids for (and upload) the chunks that changed in each commit; every unchanged chunk can be
13054a one-line lookup entry.
13055
13056Lookups are resolved **once per upload**, against the base of the first run created for it, and the resolved versions are then fixed for
13057that upload. If several runs share the same uploaded chunk set but have different bases, they all serve the versions resolved for the first
13058run — a single uploaded build serves a single set of chunk versions. Every subsequent run still re-checks that those chunks are still stored
13059(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
13060upload against a different base.
13061
13062Requirements:
13063
13064- **A base commit is required**, since lookups are resolved from the base test run's history. For GitHub projects Meticulous infers the
13065  base automatically (the same base it would compare the run against), so you normally don't need to pass anything. Pass \`--baseSha\`
13066  (or \`--repoDirectory\`, which infers it) only to override the base explicitly.
13067- **A prior chunked-asset test run must exist** in the base commit's ancestry (within 16 levels) that referenced each looked-up chunk.
13068  For the first run you must upload every chunk with explicit version ids.
13069- **The resolved chunk version must still be stored** — if it was deleted by your data retention policy, the trigger fails with an error
13070  and you'll need to re-upload that chunk with an explicit version id.
13071- **Keep \`--waitForBase\` enabled** (the default). Lookups can only resolve once the base test run exists, so the trigger polls for it;
13072  running without a base is not possible for manifests with lookup entries.
13073
13074{% callout type="warning" title="Only mark truly unchanged chunks as lookups" %}
13075It is a **hard requirement** that a chunk referenced with \`versionLookup\` is byte-for-byte unchanged relative to the base build.
13076Meticulous cannot verify this: if the chunk actually changed, the stale version from the base lineage is served silently and your test
13077results will be incorrect — the same class of failure as reusing a version id for different bytes.
13078{% /callout %}
13079
13080---
13081
13082## 3. Test it
13083
13084\`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
13085has been re-assembled correctly, with the correct versions of the correct assets mounted 
13085in the correct folders. If you have a backend
13086running 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
13087runs correctly.
13088
13089---
13090
13091## Full CI Example
13092
13093An example for a SvelteKit app;
13094
13095\`\`\`bash
13096#!/usr/bin/env bash
13097set -euo pipefail
13098
13099# Build your app into per-chunk directories (e.g. dist/charts-app, dist/plugin-1).
13100#
13101# Note: some build outputs can vary slightly between builds even if the input
13102# source files are identical.
13103#
13104# For example some builds may embed the commit SHA in one of the built assets,
13105# or some builds may not be be fully deterministic. In the case of builds that
13106# embed information like a commit SHA you may want to make sure this is split out
13107# into a seperate chunk, rather than injected into every chunk. In the case
13108# of builds that are not fully deterministic (bytes can change slightly on a rebuild)
13109# you may want to use build caching on a stable cache key to ensure chunk version
13110# ids stay stable when the outputs are functionally identical.
13111yarn build
13112
13113# Split the SvelteKit static build into two chunks along a natural boundary:
13114#   - "html": root-level pages + static assets, served at /
13115#   - "app":  the hashed _app/ bundle, served under the _app prefix
13116
13117rm -rf chunks
13118mkdir -p chunks/html
13119
13120# Root-served files (everything except the _app bundle).
13121find build -maxdepth 1 -mindepth 1 ! -name _app \\
13122  -exec cp -R {} chunks/html/ \\;
13123# The _app bundle is uploaded as its own chunk under the _app prefix.
13124
13125# Version each chunk by a content checksum (SHA1 over relative path + bytes)
13126# so chunks dedupe across commits when their contents are unchanged.
13127hash_dir() {
13128  ( cd "$1" && find . -type f -print0 | sort -z | xargs -0 sha1sum ) | sha1sum | cut -d' ' -f1
13129}
13130HTML_VERSION=$(hash_dir chunks/html)
13131APP_VERSION=$(hash_dir build/_app)
13132
13133# Upload the html chunk (short circuits if already uploaded)
13134npx -y @alwaysmeticulous/cli@latest ci upload-asset-chunk \\
13135  --chunkName=html \\
13136  --chunkVersionId="$HTML_VERSION" \\
13137  --chunkAssetsDirectory=chunks/html \\
13138  --commitSha="\${{ github.sha }}"
13139
13140# Upload the app chunk (short circuits if already uploaded)
13141npx -y @alwaysmeticulous/cli@latest ci upload-asset-chunk \\
13142  --chunkName=app \\
13143  --chunkVersionId="$APP_VERSION" \\
13144  --chunkAssetsDirectory=build/_app \\
13145  --chunkAssetsDirectoryPrefix=_app \\
13146  --commitSha="\${{ github.sha }}"
13147
13148# Trigger the run
13149cat > manifest.json <<JSON
13150[
13151  { "name": "html", "versionId": "$HTML_VERSION" },
13152  { "name": "app",  "versionId": "$APP_VERSION" }
13153]
13154JSON
13155npx -y @alwaysmeticulous/cli@latest ci run-with-uploaded-asset-chunks \\
13156  --commitSha="\${{ github.sha }}" \\
13157  --assetReferencesManifest=manifest.json \\
13158  --waitForBase=true
13159\`\`\`
13160
13161---
13162
13163## Chunk Retention
13164
13165Meticulous 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
13166the 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
13167your project (default ~90 days).
13168
13169---
13170
13171## Summary
13172
13173- Split your build into named chunks and give each a version id derived from its contents or build inputs.
13174- Upload at least the changed chunk on each commit with \`ci upload-asset-chunk\`.
13175- Reference **every** chunk required for the full build in the manifest — with an explicit \`versionId\` for changed chunks, or
13176  \`versionLookup: "latest-in-history"\` for unchanged ones — then trigger the run with \`ci run-with-uploaded-asset-chunks\`.
13177`},{id:"filter-sessions-by-start-url",url:"/docs/how-to/filter-sessions-by-start-url",document:`---
13178{
13179  "title": "Filter Sessions by Start URL"
13180}
13181---
13182
13183# {% $frontmatter.title %}
13184
13185Meticulous automatically replays the set of sessions that will exhaustively test your change. However, when triggering a
13186run with [\`ci run-with-uploaded-asset-chunks\`](/docs/how-to/incremental-asset-upload), you can pass a session filter
13187to restrict the set of sessions executed beyond that, by filtering to only sessions that start on specific routes — for
13188example to only test the part of your app affected by a change. This can be useful in extremely large applications,
13189where your build system may be able to more tightly isolate the blast radius of a change than Meticulous can via static
13190analysis.
13191
13192## Usage
13193
13194Write a JSON file with a \`session-start-url-matches-any-regex\` key listing one or more regexes:
13195
13196\`\`\`bash
13197cat > session-filter.json <<'EOF'
13198{
13199  "session-start-url-matches-any-regex": [
13200    "/checkout/",
13201    "^https://app\\\\.example\\\\.com/settings"
13202  ]
13203}
13204EOF
13205
13206npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
13207  --apiToken="$METICULOUS_API_TOKEN" \\
13208  --commitSha="$CI_COMMIT_SHA" \\
13209  --assetReferencesManifest="./assets-manifest.json" \\
13210  --sessionFilter="./session-filter.json"
13211\`\`\`
13212
13213A session is replayed if its **start URL** — the URL the session started recording on — matches **at least one** of the
13214regexes. The same filtered set of sessions is used for both the head run and any base run created to compare against, so
13215comparisons stay consistent.
13216
13217## When the filter matches no sessions
13218
13219No test run is triggered, and the CLI exits with code \`4\` (every other failure exits with \`1\`). The distinct code lets
13220a pipeline treat "this change touches no recorded flow" as a skip rather than a build failure:
13221
13222\`\`\`bash
13223set +e
13224npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks ... --sessionFilter="./session-filter.json"
13225exit_code=$?
13226set -e
13227if [ "$exit_code" -eq 4 ]; then
13228  echo "No sessions matched the filter — skipping Meticulous for this change."
13229  exit 0
13230fi
13231exit "$exit_code"
13232\`\`\`
13233
13234## Regex syntax
13235
13236Regexes use [Google's RE2 syntax](https://github.com/google/re2/wiki/Syntax). They are validated before the run is
13237triggered, so a regex that doesn't compile fails fast in the CLI with a clear error.
13238
13239{% callout type="info" title="Filtering only affects which sessions run" %}
13240The session filter narrows a single test run down from the project's selected sessions; it doesn't change which sessions
13241Meticulous records or selects. Runs triggered without \`--sessionFilter\` still replay the full selected set.
13242{% /callout %}
13243`},{id:"retry-test-run",url:"/docs/how-to/retry-test-run",document:`---
13244{
13245  "title": "Retry a test run"
13246}
13247---
13248
13249# {% $frontmatter.title %}
13250
13251In 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.
13252The Meticulous team will have already been alerted and be investigating the root cause.
13253
13254To re-run the workflow, you can always push up another commit:
13255
13256\`\`\`bash
13257git commit --allow-empty -m "Retry Meticulous" && git push
13258\`\`\`
13259
13260However sometimes it's faster to just re-run the workflow directly in your CI system. Those instructions depend on your CI provider:
13261
13262{% tabs noTabSelectedByDefault=true %}
13263{% tab label="GitHub Actions" %}
13264
13265Meticulous will show as two checks. The first shows the Meticulous results, and has the Meticulous logo next to it:
13266
13267![The Meticulous check](https://assets.meticulous.ai/docs/retry-test-run/1-meticulous-check.png)
13268
13269The second is your GitHub Actions workflow that triggers Meticulous:
13270
13271![Workflow to retrigger](https://assets.meticulous.ai/docs/retry-test-run/2-workflow-to-retrigger.png)
13272
13273It's this second workflow that you need to re-trigger. To do so click 'View Details'. It will show the workflow steps, where
13274one of those workflow steps is the step that triggers Meticulous:
13275
13276![Checking correct workflow](https://assets.meticulous.ai/docs/retry-test-run/3-checking-correct-workflow.png)
13277
13278Click the 'Re-run this job' button in the top right:
13279
13280![Re-run](https://assets.meticulous.ai/docs/retry-test-run/4-re-run.png)
13281
13282This will open a modal where you can re-run the workflow:
13283
13284![Re-run modal](https://assets.meticulous.ai/docs/retry-test-run/5-re-run-modal.png)
13285
13286{% /tab %}
13287
13288{% tab label="Other CI Runners" %}
13289
13290Use the native retry functionality in your CI provider to re-run the workflow that runs the Meticulous tests, or run:
13291
13292\`\`\`bash
13293git commit --allow-empty -m "Retry Meticulous" && git push
13294\`\`\`
13295
13296This will push up a new commit and trigger a new workflow run.
13297
13298{% /tab %}
13299{% /tabs %}
13300`},{id:"setup-okta-sso",url:"/docs/how-to/setup-okta-sso",document:`---
13301{
13302  "title": "Setting up Okta SSO for Meticulous"
13303}
13304---
13305
13306# {% $frontmatter.title %}
13307
13308This 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]\`.
13309
13310## Configuration
13311
13312Throughout these instructions, replace \`<CompanyName>\` with the name of your company.
13313
13314### Create the application
13315
13316In the *Applications* tab create a new application called *Meticulous* with:
13317
13318- Sign-in method: OIDC - OpenID Connect
13319- Application type: Web Application
13320- Sign in redirect URI:
13321
13322{% command_card_block %}
13323\`\`\`text
13324https://app.meticulous.ai/auth/realms/meticulous/broker/SSO_<CompanyName>/endpoint
13325\`\`\`
13326{% /command_card_block %}
13327
13328- Sign out redirect URI:
13329
13330{% command_card_block %}
13331\`\`\`text
13332https://app.meticulous.ai/auth/realms/meticulous/broker/SSO_<CompanyName>/endpoint/logout_response
13333\`\`\`
13334{% /command_card_block %}
13335
13336### Configure the application
13337
13338Once the application is created, configure it with the following settings:
13339
13340- 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.
13341- Grab our logo from \`https://app.meticulous.ai/logo512.png\` and add it to the application.
13342- Login initiated by: Either Okta or App
13343- Application visibility: Display application icon to users
13344- Login flow: Redirect to app to initiate login (OIDC Compliant)
13345- Initiate login URI:
13346
13347{% command_card_block %}
13348\`\`\`text
13349https://app.meticulous.ai/api/auth/signin?next=https%3A%2F%2Fapp%2Emeticulous%2Eai&idp=SSO_<CompanyName>
13350\`\`\`
13351{% /command_card_block %}
13352
13353### Send the details to Meticulous
13354
13355Send 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:
13356
13357  1. The client ID of the application.
13358  2. The client secret of the application.
13359  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/\`).
13360  4. A list of domain name(s) your users' emails might have.
13361  5. What value you used for \`<CompanyName>\`.
13362
13363### [Optional] Configure the SSO claims
13364
13365If 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:
13366
13367- \`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.
13368- \`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.
13369
13370If 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.
13371
13372## FAQs
13373
13374- **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.
13375
13376- **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.
13377
13378- **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.
13379`},{id:"use-custom-event-api",url:"/docs/how-to/use-custom-event-api",document:eL},{id:"recorder-developer-tools",url:"/docs/how-to/recorder-developer-tools",document:eN},{id:"enabling-full-auth",url:"/docs/how-to/auth/enabling-full-auth",document:eX},{id:"bypassing-auth",url:"/docs/how-to/auth/bypassing-auth",document:eQ},{id:"recorder-npm-dependency",url:"/docs/session-recording/recorder-npm-dependency",document:e5},{id:"ingest-existing-tests",url:"/docs/session-recording/ingest-existing-tests",document:e4},{id:"controlling-data-recorded",url:"/docs/session-recording/controlling-data-recorded",document:e6},{id:"controlling-when-recording-starts-and-stops",url:"/docs/session-recording/controlling-when-recording-starts-and-stops",document:tt},{id:"csp-exceptions",url:"/docs/session-recording/csp-exceptions",document:`---
13380{
13381  "title": "Content Security Policy (CSP) exceptions for the recorder snippet"
13382}
13383---
13384
13385# {% $frontmatter.title %}
13386
13387If you have a strict Content Security Policy (CSP) in place, you may 
13387need to add the following exceptions to allow the Meticulous recorder to work correctly:
13388
13389 - \`frame-src\`: https://snippet.meticulous.ai
13390 - \`script-src\`: https://snippet.meticulous.ai
13391 - \`script-src\`: https://browser.sentry-cdn.com
13392 - \`connect-src\`: https://cognito-identity.us-west-2.amazonaws.com
13393 - \`connect-src\`: https://user-events-v3.s3-accelerate.amazonaws.com
13394 - \`connect-src\`: *.sentry.io
13395`},{id:"redaction",url:"/docs/session-recording/redaction",document:ts},{id:"nextjs-app-router",url:"/docs/frameworks/nextjs/app-router",document:to},{id:"nextjs-pages-router",url:"/docs/frameworks/nextjs/pages-router",document:tn},{id:"react-vite",url:"/docs/frameworks/react/vite",document:ti},{id:"react-create-react-app",url:"/docs/frameworks/react/create-react-app",document:ta},{id:"vue-vite",url:"/docs/frameworks/vue/vite",document:tr},{id:"angular-cli",url:"/docs/frameworks/angular/angular-cli",document:tl},{id:"create-deployments-on-github",url:"/docs/alternative-ci-setups/create-deployments-on-github",document:tc},{id:"exporting-generated-tests",url:"/docs/export/exporting-generated-tests",document:`---
13396{
13397  "title": "Exporting generated tests"
13398}
13399---
13400
13401# {% $frontmatter.title %}
13402
13403If you wish to re-use the Meticulous tests in another tool there are three main ways you can export tests from Meticulous:
13404
13405  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)
13406  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)
13407  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)
13408
13409Sessions (tests) are returned as JSON, and have two main components:
13410
13411 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
13412 testing frameworks that support network stubbing, and are supported natively in Chrome, Safari, Firefox and Edge.
13413 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
13414  frameworks, or by calling \`dispatchEvent\` in a browser.
13415`},{id:"not-yet-run-checks",url:"/docs/ci/not-yet-run-checks",document:`---
13416{
13417  "title": "Create Meticulous check in 'success' state until tests start running"
13418}
13419---
13420
13421# {% $frontmatter.title %}
13422
13423In the default Meticulous setup you'll have a workflow that builds your app and invokes the
13424[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
13425make Meticulous a blocking check by marking the '*${tu.METICULOUS_GITHUB_CHECK_NAME}*' check as
13426a [required check](${o.MAKE_CHECK_BLOCKING_URL}). The build process would therefore look like this:
13427
13428 1. Your '*trigger-meticulous-tests.yml*' GitHub workflow is triggered (e.g. when a PR is opened). The '*${tu.METICULOUS_GITHUB_CHECK_NAME}*' check
13429    has not been created yet, since the Meticulous tests have not started yet, and since you've marked '*${tu.METICULOUS_GITHUB_CHECK_NAME}*' as
13430    a required check the PR will not be able to be merged yet.
13431 2. Once the build and pre-steps complete, the Meticulous action is invoked. This creates a second check on the PR, normally
13432    named '*${tu.METICULOUS_GITHUB_CHECK_NAME}*'. This check will show as pending until the tests complete. If there are unapproved differences it
13433    will show as a failure, and will be updated to success when the differences are approved. Since you've marked the '*${tu.METICULOUS_GITHUB_CHECK_NAME}*'
13434    check as a required check, the PR will not be able to be merged until the differences are approved.
13435 3. Finally the '*trigger-meticulous-tests.yml*' workflow will complete, and be marked as success.
13436
13437{% callout type="warning" %}
13438**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 *'${tu.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.
13439{% /callout %}
13440
13441However having the '*${tu.METICULOUS_GITHUB_CHECK_NAME}*' check as a required check can cause issues in a couple of scenarios:
13442
13443 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,
13444    since that would block the merge queue from merging. Any differences should have already been approved before the PR was added to the
13445    merge queue. It is therefore standard to skip the Meticulous workflow for merge queue triggers. However, if Meticulous is a
13446    [required checks](${o.MAKE_CHECK_BLOCKING_URL}) then the merge queue would be indefinitely blocked because it'd be waiting for a check
13447    that is never created.
13448 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
13449    (i.e. no Meticulous check was ever created).
13450
13451There are two ways of solving these issues:
13452
13453 1. Tick the '*${td.INITIALIZE_WITH_SUCCESSFUL_CHECK_CHECKBOX_LABEL}*' option in your Meticulous project settings.
13454    This will cause Meticulous to register GitHub webhooks to monitor for new pull requests, new commit pushes, and for pull requests added
13455    to merge queues. It will then create a successful '*${tu.METICULOUS_GITHUB_CHECK_NAME}*' check in each of these cases straight away. This
13456    check will start off as 'success' and turn to 'pending' when and if the Meticulous tests start running. If the Meticulous tests never run
13457    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
13458    the period between the PR being opened and the Meticulous tests being triggered after the build or deployment completes.
13459 2. Use a GitHub action such as [wait-for-checks](https://github.com/marketplace/actions/wait-for-checks) that only waits for checks that
13460    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
13461    as a GitHub workflow is running or a check pending while the application is being built prior to the Meticulous tests being triggered then
13462    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
13463    deployment then there is a risk the PR will be mergeable in the handful of seconds between the workflow that creates the deployment completing
13464    and Meticulous receiving the GitHub webhook for the new deployment and starting the tests.
13465`},{id:"agents-setup",url:"/docs/agents/setup",document:`---
13466{
13467  "title": "Setting up Meticulous for agents"
13468}
13469---
13470
13471# {% $frontmatter.title %}
13472
13473Meticulous exposes the same read, analysis, and test-run-triggering operations to agents in a few different ways — pick whichever fits how your agent connects.
13474
13475- [CLI](#cli) — a global \`meticulous\` binary the agent runs in your terminal.
13476- [MCP](#mcp) — the hosted MCP server, for agents that speak MCP.
13477- [Skills](#skills) — pre-defined workflows on top of any of the above.
13478- [Ready to use integrations](#ready-to-use-integrations) — one-click installs for Claude Code and Cursor.
13479- [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.
13480
13481---
13482
13483## CLI
13484
13485To set up the Meticulous CLI:
13486
13487\`\`\`bash
13488npm install --global @alwaysmeticulous/cli@latest
13489meticulous auth login
13490\`\`\`
13491
13492See [CLI commands](${o.AGENTS_CLI_COMMANDS_URL}) for more details, including more authentication options and the command reference.
13493
13494---
13495
13496## MCP
13497
13498**Claude Code**
13499
13500Run the following in your terminal:
13501
13502\`\`\`bash
13503claude mcp add --transport http Meticulous https://app.meticulous.ai/api/mcp
13504\`\`\`
13505
13506Then, in Claude Code, type \`/mcp\` and choose "Authenticate" for the Meticulous MCP.
13507
13508**Cursor**
13509
13510Add to \`~/.cursor/mcp.json\` (global) or \`.cursor/mcp.json\` (per project):
13511
13512\`\`\`json
13513{
13514  "mcpServers": {
13515    "Meticulous": { "url": "https://app.meticulous.ai/api/mcp" }
13516  }
13517}
13518\`\`\`
13519
13520**Codex/ChatGPT**
13521
13522Add a server with name "Meticulous" and URL \`https://app.meticulous.ai/api/mcp\`, then click "Authenticate".
13523
13524See [MCP server](${o.AGENTS_MCP_SERVER_URL}) for more details, including the full list of available tools.
13525
13526---
13527
13528## Skills
13529
13530To install, and update, the skills into your project using [npx skills](https://github.com/vercel-labs/skills) (for the specified agents):
13531
13532\`\`\`bash
13533npx skills add alwaysmeticulous/skills --skill "*" --agent claude-code --agent codex --agent cursor -y
13534\`\`\`
13535
13536See [Skills & Use cases](${o.AGENTS_SKILLS_URL}) for what skills are, the full list of them, and what each one is for.
13537
13538---
13539
13540## Ready to use integrations
13541
13542**Claude Code MCP**
13543
13544Install the hosted MCP server directly from the [Claude directory](https://claude.ai/directory/meticulous).
13545
13546**Claude Code plugin**
13547
13548For 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:
13549
13550\`\`\`shell
13551/plugin marketplace add alwaysmeticulous/skills
13552\`\`\`
13553
13554Then install the Meticulous plugin:
13555
13556\`\`\`shell
13557/plugin install meticulous@meticulous
13558\`\`\`
13559
13560Then type \`/mcp\` and choose "Authenticate" for the Meticulous MCP server.
13561
13562**Cursor plugin**
13563
13564Install the MCP server and skills directly from the [Cursor marketplace](https://cursor.com/marketplace/meticulous).
13565
13566---
13567
13568## Org-wide setup {% #org-wide-setup %}
13569
13570The steps above set up Meticulous for a single user. To roll it out to your whole team, there are two approaches:
13571
13572- **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.
13573- **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).
13574
13575To set up an org-wide MCP connector, navigate to:
13576
13577- **Claude Code:** Organization Settings > Connectors > Add (Web).
13578- **Cursor:** Settings > Integrations & MCP > Team MCP Servers > Add (Remote HTTPS).
13579
13580To configure it with a project API token, so it authenticates every user centrally, do the following:
13581
13582- **URL:** \`https://app.meticulous.ai/api/mcp\`
13583- **Credential type:** Bearer
13584- **Prefix:** \`Bearer\`
13585- **Value:** a project API token
13586
13587Select the project below, then copy the token (careful: grants access to recorded sessions!):
13588
13589{% code_with_project_selector %}
13590{% standalone_api_token /%}
13591{% /code_with_project_selector %}
13592
13593As a concrete example of the general steps above, for Claude Tag (Claude in Slack):
13594
135951. Go to [claude.ai/admin-settings/claude-tag/access-bundles](https://claude.ai/admin-settings/claude-tag/access-bundles).
135962. Under **General > Credentials**, click **Connect**.
135973. Set Name to \`Meticulous\` and Credential type to \`Bearer\`.
135984. Set Allowed websites to \`app.meticulous.ai\`.
135995. Under **Custom headers**, click **Add header**, and set:
13600   - Name: \`Authorization\`
13601   - Prefix: \`Bearer\`
13602   - Value: the project API token created above.
13603
13604---
13605
13606{% footnote %}
13607*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.*
13608{% /footnote %}
13609`},{id:"agents-setup-for-agents",url:"/docs/agents/setup-for-agents",document:`---
13610{
13611  "title": "Meticulous for coding agents"
13612}
13613---
13614
13615# {% $frontmatter.title %}
13616
13617This 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}
13617) instead — it covers the same ground, aimed at you rather than at the agent.
13618
13619{% agent_instructions_heading /%}
13620
13621${(0,tu.buildAgentInstructionsBody)()}
13622`},{id:"agent-review",url:"/docs/agents/agent-swarm",document:`---
13623{
13624  "title": "Set up Agent swarm"
13625}
13626---
13627
13628# {% $frontmatter.title %}
13629
13630Agent 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.
13631
13632Use 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.
13633
13634{% callout type="warning" title="Contact us before adding this to CI" %}
13635Agent 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.
13636
13637When 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).
13638{% /callout %}
13639
13640## Before you start
13641
13642You need:
13643
13644- A Meticulous project linked to the repository and opted in to the beta (see above).
13645- A project-scoped API token stored in your CI provider as \`METICULOUS_API_TOKEN\`.
13646- A build command that produces a directory of static frontend assets.
13647- A GitHub Actions workflow (or equivalent CI job) that runs when a pull request is opened, plus a way to re-run manually.
13648
13649Choose one environment for the agent:
13650
13651| Your application | Recommended setup |
13652| --- | --- |
13653| Static site, or frontend whose API responses can be mocked | Upload the build only. |
13654| Static frontend with a disposable staging API | Upload the build and proxy selected paths, such as \`/api\`, to that API. |
13655| App that can run in a Docker image | Upload the container with \`--localImageTag\`; see the [CLI command reference](${o.AGENTS_CLI_COMMANDS_URL}). |
13656
13657The rest of this guide walks through the first two options.
13658
13659---
13660
13661## 1. Add agent instructions
13662
13663Create \`.github/agent-review/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.
13664
13665For example:
13666
13667\`\`\`md
13668# Storefront
13669
13670Start 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.
13671\`\`\`
13672
13673Good 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.
13674
13675If 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.
13676
13677For 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.
13678
13679---
13680
13681## 2. Build and launch the static frontend
13682
13683Add 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.
13684
13685The following GitHub Actions job builds a static site in \`build/\` and launches Agent swarm for the PR's **head commit**:
13686
13687\`\`\`yaml
13688name: Agent swarm
13689
13690on:
13691  pull_request:
13692    types: [opened, reopened]
13693  workflow_dispatch:
13694
13695jobs:
13696  generate-sessions:
13697    runs-on: ubuntu-latest
13698    env:
13699      METICULOUS_API_TOKEN: \${{ secrets.METICULOUS_API_TOKEN }}
13700    steps:
13701      - uses: actions/checkout@v4
13702      - uses: pnpm/action-setup@v4
13703      - uses: actions/setup-node@v4
13704        with:
13705          node-version: "22"
13706          cache: pnpm
13707      - run: pnpm install --frozen-lockfile
13708      - run: pnpm build
13709      - name: Launch Agent swarm
13710        run: |
13711          npx -y @alwaysmeticulous/cli@latest ci agent-test \\
13712            --assetsDir build \\
13713            --commitSha "\${{ github.event.pull_request.head.sha || github.sha }}" \\
13714            --instructionsFile .github/agent-review/instructions.md
13715\`\`\`
13716
13717Replace \`build\` with your build output directory. The command runs the agent in Meticulous; your CI runner does not need Docker or LLM credentials.
13718
13719Do **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**.
13720
13721{% callout type="warning" title="Use the pull request head SHA" %}
13722On 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.
13723{% /callout %}
13724
13725To 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.
13726
13727---
13728
13729## 3. Connect a staging backend (when needed)
13730
13731If 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:
13732
13733\`\`\`yaml
13734      - name: Launch Agent swarm with staging API
13735        env:
13736          METICULOUS_STAGING_USERNAME: \${{ secrets.STAGING_AGENT_USERNAME }}
13737          METICULOUS_STAGING_PASSWORD: \${{ secrets.STAGING_AGENT_PASSWORD }}
13738        run: |
13739          npx -y @alwaysmeticulous/cli@latest ci agent-test \\
13740            --assetsDir frontend/dist \\
13741            --backendUrl "https://staging.example.com" \\
13742            --backendProxyPaths /api \\
13743            --commitSha "\${{ github.event.pull_request.head.sha || github.sha }}" \\
13744            --instructionsFile .github/agent-review/instructions.md
13745\`\`\`
13746
13747Your 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.
13748
13749### Login setup
13750
13751If 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):
13752
13753| Tell us | Why we need it |
13754| --- | --- |
13755| How users sign in (username/password on the app, redirect to IdP/SSO, magic link, etc.) | Chooses / customizes the login flow; popups and many OAuth/SSO flows are not supported today |
13756| 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 |
13757| 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"]\` |
13758| Submit control (button text / selector) | Same as above |
13759| What "logged in" looks like (URL after login, cookie names, or a screenshot) | Confirms the flow succeeded before the agent starts |
13760| 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 |
13761| Staging origin (\`https://…\`) | Wired as \`--backendUrl\` |
13762| Whether MFA / captcha / bot checks apply to the test account | Usually blocks automated login unless disabled for that account |
13763
13764You can paste this into Slack or email:
13765
13766\`\`\`text
13767Project: <org/project>
13768Staging backend URL: https://…
13769Login type: username/password on app | SSO/IdP | other (describe)
13770Login URL/path: …
13771Username field: (default ok / selector: …)
13772Password field: (default ok / selector: …)
13773Submit: (default ok / selector or button text: …)
13774After login: lands on … / session cookie …
13775Post-login tour/tip: none | completion action/state: …
13776Test account: (username/password go in CI secrets only)
13777MFA/captcha on staging for this account: yes/no
13778\`\`\`
13779
13780Once 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.
13781
13782---
13783
13784## 4. Verify the first run
13785
137861. Open a pull request with a small, visible change (or manually re-run the workflow).
137872. Confirm the **Agent swarm** job uploads the build and prints a workflow run identifier.
137883. Open the Meticulous test run for the PR commit, then open **Agent swarm** to inspect the generated test cases, results, and screenshots.
137894. Review the recorded flows and adjust \`instructions.md\` if important routes or states were missed.
13790
13791If 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}).
13792`},{id:"agents-whats-new",url:"/docs/agents/whats-new",document:`---
13793{
13794  "title": "What's new"
13795}
13796---
13797
13798# {% $frontmatter.title %}
13799
13800Unless a change is specific to one surface, changes listed here apply equally to the CLI and the corresponding MCP tools.
13801
13802### September 15, 2026 — retrieve, filter and order the selected set
13803
13804- \`meticulous agent sessions --selectedSet\` narrows the listing to the selected set Meticulous replays.
13805- \`--includeSelectedSince\` adds when each session entered that set.
13806- \`--includeAdditionalCoverage\` adds the coverage each session contributed over everything picked before it.
13807- \`--orderBy\` chooses the order (\`rank\`, \`selectedSince\`, \`additionalCoverage\`), and \`--order\` sets the direction (\`asc\`/\`desc\`).
13808
13809\`\`\`bash
13810meticulous agent sessions --selectedSet --includeSelectedSince
13811meticulous agent sessions --selectedSet --orderBy=rank
13812meticulous agent sessions --selectedSet="2026-07-01"
13813meticulous agent sessions --selectedSet --orderBy=selectedSince --order=asc
13814meticulous agent sessions --selectedSet --includeAdditionalCoverage --orderBy=additionalCoverage
13815\`\`\`
13816
13817---
13818
13819### September 11, 2026 — export reporting statistics
13820
13821Three 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.
13822
13823\`\`\`bash
13824meticulous agent test-run-stats --since="2026-08-01"
13825meticulous agent project-daily-stats --since="2026-08-01"
13826meticulous agent test-run-event-stats --testRunIds="<id1>,<id2>" --json
13827\`\`\`
13828
13829---
13830
13831### September 11, 2026 — coverage you can triage, total, and compare
13832
13833- \`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.
13834- \`--includeLineCounts\` **(new)** adds per-file \`executedLines\`/\`executableLines\`/\`uncoveredLines\` instead of ranges.
13835- \`--orderBy\` and \`--limit\`/\`--offset\` **(new)** order and page the output server-side.
13836- \`--globFilter\` is now repeatable, matching any of several globs.
13837- \`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\`.
13838
13839\`\`\`bash
13840meticulous agent js-coverage --summary
13841meticulous agent js-coverage --includeLineCounts --orderBy=uncoveredLines --limit=50
13842meticulous agent js-coverage-diff --summary
13843\`\`\`
13844
13845---
13846
13847### September 4, 2026 — link to a group of diffs
13848
13849- 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.
13850- 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.
13851
13852\`\`\`bash
13853meticulous agent test-run-diffs --similarGroupId="<id>" --testRunId="<id>"
13854\`\`\`
13855
13856---
13857
13858### August 17, 2026 — get real coverage for a base commit
13859
13860- \`meticulous agent complete-base-run\` **(new)** replays the rest of a base run's selected sessions, to complete its coverage information.
13861- \`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.
13862
13863\`\`\`bash
13864meticulous agent complete-base-run
13865meticulous agent js-coverage
13866\`\`\`
13867
13868---
13869
13870### August 11, 2026 — discover available check IDs, and a renamed check command
13871
13872- \`meticulous agent test-run-checks\` is renamed to \`meticulous agent test-run-check\`, since it operates on a single check.
13873- The new \`--availableIds\` flag (MCP: \`get_test_run_check_available_ids\`) lists the check IDs that have reported results for a test run.
13874
13875\`\`\`bash
13876meticulous agent test-run-check --availableIds --testRunId="<id>"
13877meticulous agent test-run-check --checkId="accessibility" --testRunId="<id>"
13878\`\`\`
13879
13880---
13881
13882### August 10, 2026 — agent diff reviews and review comment writes
13883
13884- \`meticulous agent reject-diff\` records a rejection with a review comment explaining why.
13885- \`meticulous agent ignore-diff\` says a diff looks like an unrelated variant / flake, currently as a comment only.
13886- The new \`create-diff-comment\` and \`reply-to-diff-comment\` commands let agents start and continue review threads independently of a decision.
13887- \`meticulous agent diff-comments\` gained an \`isAgentAuthored\` attribute, distinguishing agent-written comments from human ones.
13888
13889\`\`\`bash
13890meticulous agent reject-diff --replayDiffId="<id>" --screenshotName="<name>" --reason="..." --x=0.5 --y=0.5
13891meticulous agent reply-to-diff-comment --commentId="<id>" --text="..."
13892\`\`\`
13893
13894---
13895
13896### August 7, 2026 — non-visual check reports, session activity counts, and see and change which project you're querying
13897
13898- \`meticulous agent test-run-checks\` retrieves the Markdown report for a non-visual check. For customer-reported checks, use \`--checkType="custom"\`.
13899- \`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).
13900- 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\`.
13901- 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.
13902
13903\`\`\`bash
13904meticulous agent test-run-checks --checkId="accessibility"
13905meticulous agent test-run-checks --checkId="network-requests" --testRunId="<id>"
13906meticulous agent sessions --includeDurationSeconds
13907meticulous agent sessions --includeNumberUserEvents
13908meticulous agent sessions --includeNumberUrlsVisited
13909meticulous auth get-project --json
13910\`\`\`
13911
13912---
13913
13914### August 4, 2026 — filter and focus test-run diffs
13915
13916- \`meticulous agent test-run-diffs --onlyWithComments\` filters to screenshot diffs with at least one open review comment.
13917- The \`--only*\` row filters now combine as a union, so passing several returns the diffs matching any of them.
13918- \`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.
13919- \`meticulous agent test-run-diffs --counts\` now also reports \`numWithOpenComments\`.
13920
13921---
13922
13923### August 3, 2026 — review comments, rejected diffs, and leaner test-run-diffs output
13924
13925- \`meticulous agent test-run-diffs --includeReviews\` adds \`decision\` (previously \`--includeReviewDecisions\`) and \`openComments\` to each diff.
13926- \`meticulous agent diff-comments --replayDiffId="<id>" --screenshotName="<name>"\` retrieves the corresponding open comments with nested replies.
13927- \`meticulous agent test-run-diffs --onlyRejected\` returns every screenshot diff already marked rejected, across every difference rather than only the selected subset.
13928- \`meticulous agent test-run-diffs\` no longer returns \`index\` or \`outcome\`, and now returns \`mismatchFraction\` only with \`--includeMismatchFraction\`.
13929
13930\`\`\`bash
13931meticulous agent test-run-diffs --includeReviews
13932meticulous agent diff-comments --replayDiffId="<id>" --screenshotName="<name>"
13933meticulous agent test-run-diffs --onlyRejected
13934meticulous agent test-run-diffs --includeMismatchFraction
13935\`\`\`
13936
13937---
13938
13939### July 24, 2026 — submit feedback about Meticulous
13940
13941- \`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\`.
13942
13943\`\`\`bash
13944meticulous agent submit-feedback --message="Caught a real regression in the checkout flow" --outcome="helped" --testRunId="<id>" --skill="meticulous-review"
13945\`\`\`
13946
13947---
13948
13949### July 21, 2026 — OAuth device flow login and project-level JS coverage
13950
13951- \`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.
13952- \`meticulous agent js-coverage\` gained \`--latestForProject\`, which returns per-file coverage from the project's preferred latest successful test run.
13953
13954---
13955
13956### July 20, 2026 — list a project's recently recorded sessions
13957
13958- \`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.
13959- \`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.
13960
13961\`\`\`bash
13962meticulous agent sessions
13963meticulous agent sessions --createdSince="2026-07-01" --createdUntil="2026-07-10"
13964meticulous agent sessions --recordedBy="[email protected]" --visitedUrlFilter="*/checkout*"
13965meticulous agent sessions --recordedSince 2026-07-10 --excludeSyntheticSessions --l
13965imit 10
13966meticulous agent trigger-test-run --sessionIds="<id1>,<id2>" --maxDurationSeconds=none
13967\`\`\`
13968
13969---
13970
13971### July 16, 2026 — MCP server for agents
13972
13973The 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.
13974
13975---
13976
13977### July 13, 2026 — review-state aware test-run-diffs, diff counts, and per-account default project
13978
13979- \`meticulous agent test-run-diffs\` now understands PR review state, and can report totals without the full list:
13980
13981  | Flag | What it does |
13982  |------|--------------|
13983  | \`--includeReviewDecisions\` | Add a \`decision\` column with each diff's PR review decision (\`accepted\`/\`rejected\`/\`ignored\`/\`unreviewed\`; \`unreviewed\` when undecided or there's no PR) |
13984  | \`--onlyUnreviewed\` | Return only the diffs still awaiting review — everything left to look at, across every difference (implies \`--includeAllDiffs\`, so the \`isSelected\` column is included) |
13985  | \`--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 |
13986
13987- Your default project is now a per-account setting, too:
13988  - \`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.
13989  - \`meticulous auth get-project\` **(new)** prints your default project, which you can also view and change from your user settings in the web app.
13990  - \`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.
13991
13992  \`\`\`bash
13993  meticulous auth set-project --project="my-org/my-project"
13994  meticulous auth get-project
13995  meticulous agent js-coverage --project="my-org/my-project"
13996  \`\`\`
13997
13998---
13999
14000### July 10, 2026 — test-run-diffs is differences-only
14001
14002\`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.
14003
14004---
14005
14006### July 7, 2026 — get combined coverage from multiple test runs
14007
14008\`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.
14009
14010\`\`\`bash
14011meticulous agent js-coverage --headPlusTestRunIds="<id1>,<id2>"
14012meticulous agent js-coverage --testRunIds="<id1>,<id2>,<id3>"
14013\`\`\`
14014
14015---
14016
14017### July 6, 2026 — consistent machine-readable output for agent & auth
14018
14019- \`agent\` and \`auth\` commands gained \`--json\` for JSON-structured output instead of default format.
14020- \`agent\` and \`auth\` commands also gained \`--verbose\`, which prints additional progress logs on stderr.
14021- \`--rawJson\` is renamed to \`--jsonArgs\` (old name still works, now deprecated).
14022
14023\`\`\`bash
14024meticulous agent js-coverage --json
14025meticulous auth whoami --json
14026\`\`\`
14027
14028---
14029
14030### July 1, 2026 — more coverage info, session pinning, and non-interactive login
14031
14032- \`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).
14033- \`meticulous agent trigger-test-run\` can now run with no arguments at all — it infers the already-uploaded deployment for your local HEAD commit.
14034- \`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.
14035- \`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.
14036- \`meticulous auth login --non-interactive\` lets the login flow run without a TTY: it prints the login URL instead of opening a browser.
14037
14038\`\`\`bash
14039meticulous agent js-coverage --includeCoveragePercentage --prDiffOnly
14040meticulous agent trigger-test-run
14041meticulous agent trigger-test-run --deploymentId="<id>
14041" --baseSha="<base-sha>" --sessionIds="<id1>,<id2>"
14042meticulous agent trigger-test-run --commitSha="<sha>" --baseSha="<base-sha>"
14043meticulous auth login --non-interactive --project="my-org/my-project"
14044\`\`\`
14045
14046---
14047
14048### June 29, 2026 — separate build upload from triggering a test run
14049
14050Two 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.
14051
14052\`\`\`bash
14053# Upload a static build (or a container image) and capture the deploymentId
14054meticulous agent upload-build --appDirectory="<path-to-build>"
14055meticulous agent upload-build --localImageTag="<image-tag>"
14056
14057# Trigger a run against an uploaded build
14058meticulous agent trigger-test-run --deploymentId="<id>"
14059\`\`\`
14060
14061---
14062
14063### June 24, 2026 — smoother authentication and non-interactive project selection
14064
14065Authentication is easier to drive from scripts and agents, and a stored login is no longer shadowed by a stale token.
14066
14067- 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.
14068- \`meticulous auth login\` **(new)** forces a fresh browser login and then selects a project.
14069- \`meticulous auth whoami\` now also reports which credential is actually in use.
14070- \`meticulous auth logout\` now also clears the selected project, and warns if an environment-variable or config-file token will keep being used.
14071- \`meticulous auth list-projects\` **(new)** lists the projects you can access.
14072- Argument \`--project org/project\` **(new)** on \`login\` / \`set-project\` lets you select a project non-interactively.
14073
14074\`\`\`bash
14075meticulous auth login --project="my-org/my-project"
14076meticulous auth list-projects
14077\`\`\`
14078
14079---
14080
14081### June 19, 2026 — richer, curated test-run-diffs output
14082
14083By 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:
14084
14085| Flag | What it does |
14086|------|--------------|
14087| \`--includeDomDiffIds\` | Include DOM-diff IDs for each screenshot |
14088| \`--includeAllDiffs\` | Return every diff, not just the curated set (adds an \`isSelected\` column) |
14089| \`--includeMatches\` | Include matching and known-flaky screenshots too, not just differences (implies \`--includeAllDiffs\`) |
14090| \`--orderByReplayDiffs\` | Order by replay then event index instead of priority |
14091
14092Polling output is also quieter, and runs that can't produce diffs now fail fast with a clear message.
14093
14094---
14095
14096### June 12, 2026 — JavaScript coverage and lookup by commit
14097
14098New 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.
14099
14100\`\`\`bash
14101meticulous agent js-coverage          # coverage for a test run (defaults to the current git HEAD)
14102meticulous agent js-coverage-diff     # base-vs-head coverage diff for a replay diff
14103meticulous agent test-run-for-commit  # the latest test run for the current commit
14104\`\`\`
14105`},{id:"agents-cli-commands",url:"/docs/agents/cli-commands",document:`---
14106{
14107  "title": "CLI commands for agents"
14108}
14109---
14110
14111# {% $frontmatter.title %}
14112
14113The 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.
14114
14115- [Install & update the Meticulous CLI](#install-update-the-meticulous-cli)
14116- [Authentication](#authentication)
14117- [Command reference](#command-reference)
14118
14119---
14120
14121## Install & update the Meticulous CLI
14122
14123To install, and update, the Meticulous CLI:
14124
14125\`\`\`bash
14126npm install --global @alwaysmeticulous/cli@latest
14127\`\`\`
14128
14129You can also install it locally per-project instead of globally.
14130
14131The CLI is under active development with frequent changes and improvements — re-run the same command to update the CLI to the latest version.
14132
14133---
14134
14135## Authentication
14136
14137Authenticate the CLI with your Meticulous account:
14138
14139\`\`\`bash
14140meticulous auth login
14141\`\`\`
14142
14143This 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
14143RL}) 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:
14144
14145\`\`\`bash
14146meticulous auth login --non-interactive --project <organization/project>
14147meticulous auth set-project --project <organization/project>
14148\`\`\`
14149
14150\`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.
14151
14152If 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.
14153
14154\`\`\`bash
14155meticulous auth login --device --project <organization/project>
14156\`\`\`
14157
14158{% expand title="Alternative: using an API token" %}
14159Select the project below that contains the sessions you wish to work with, then copy the token:
14160
14161{% code_with_project_selector %}
14162METICULOUS_API_TOKEN:
14163{% standalone_api_token /%}
14164{% /code_with_project_selector %}
14165
14166*Be very careful with this API token, since it allows the holder access to your recorded sessions.*
14167
14168Pass it to the CLI in one of two ways:
14169
14170- Set the \`METICULOUS_API_TOKEN\` environment variable:
14171
14172  \`\`\`bash
14173  export METICULOUS_API_TOKEN="<paste-token-here>"
14174  \`\`\`
14175
14176- Or store it in \`~/.meticulous/config.json\`:
14177
14178  \`\`\`json
14179  { "apiToken": "<paste-token-here>" }
14180  \`\`\`
14181{% /expand %}
14182
14183---
14184
14185## Command reference
14186
14187### Global command options
14188
14189\`\`\`bash
14190meticulous <command> --json               # output in JSON format on stdout instead of default format
14191meticulous <command> --jsonArgs="<json>"  # pass all options as a JSON string
14192meticulous <command> --verbose            # also print progress to stderr (instead of just warnings)
14193meticulous <command> --dryRun             # print what a mutating command would do, without doing it
14194\`\`\`
14195
14196---
14197
14198### Authentication
14199
14200\`\`\`bash
14201meticulous auth login          # force a fresh browser login, then select a project (interactive)
14202meticulous auth login --non-interactive --project="{org}/{proj}"  # same, but non-interactive
14203meticulous auth login --device --project="{org}/{proj}"  # same, but OAuth device flow
14204meticulous auth whoami         # show how you're currently authenticated
14205meticulous auth logout         # revoke and clear stored tokens
14206meticulous auth get-project    # print your default project
14207meticulous auth set-project    # choose your default project (interactive)
14208meticulous auth set-project --project="{org}/{proj}"  # same, but non-interactive
14209meticulous auth list-projects  # list the projects you can access
14210\`\`\`
14211
14212---
14213
14214### Discover the CLI surface
14215
14216\`\`\`bash
14217meticulous schema           # full schema
14218meticulous schema simulate  # narrow to a single command or group
14219\`\`\`
14220
14221---
14222
14223### Look up the test run for a commit
14224
14225\`\`\`bash
14226meticulous agent test-run-for-commit                      # latest run for the current git HEAD
14227meticulous agent test-run-for-commit --commitSha="<sha>"  # …or for a specific commit
14228meticulous agent test-run-for-commit --dontWaitForTestRunToComplete  # don't block on in-progress runs
14229meticulous agent test-run-for-commit --project="{org}/{proj}"  # override default project for this call
14230\`\`\`
14231
14232---
14233
14234### Retrieve diffs for a test run
14235
14236\`\`\`bash
14237meticulous agent test-run-diffs                       # diffs for test run on current commit
14238meticulous agent test-run-diffs --testRunId="<id>"    # …or for an explicit test run
14239meticulous agent test-run-diffs --commitSha="<sha>"   # …or for a specific commit
14240meticulous agent test-run-diffs --dontWaitForTestRunToComplete  # don't block on in-progress runs
14241meticulous agent test-run-diffs --includeReplayIds    # include base and head replay IDs per diff
14242meticulous agent test-run-diffs --includeMismatchFraction  # include the fraction of changed pixels
14243meticulous agent test-run-diffs --includeReviews        # add decision and comment count per diff
14244meticulous agent test-run-diffs --includeDomDiffIds   # include DOM-diff IDs per screenshot
14245meticulous agent test-run-diffs --includeSimilarGroupId  # add the Similar group id per diff
14246meticulous agent test-run-diffs --similarGroupId="<id>"  # every diff in a Similar group
14247meticulous agent test-run-diffs --includeAllDiffs     # every diff, not just the selected set
14248meticulous agent test-run-diffs --onlyUnreviewed      # only diffs awaiting review
14249meticulous agent test-run-diffs --onlyRejected        # all rejected diffs
14250meticulous agent test-run-diffs --onlyWithComments    # only diffs with open review comments
14251meticulous agent test-run-diffs --orderByReplayDiffs  # group by replay diff instead of priority order
14252meticulous agent test-run-diffs --project="{org}/{proj}"  # override default project for this call
14253meticulous agent test-run-diffs --counts              # just the total counts, not the full diff list
14254\`\`\`
14255
14256---
14257
14258### Investigate a diff in more detail
14259
14260\`\`\`bash
14261# Download screenshots to ~/.meticulous/agent-images/, or get image URLs
14262meticulous agent image-files --replayDiffId="<id>" --screenshotName="<name>"
14263meticulous agent image-urls --replayDiffId="<id>" --screenshotName="<name>"
14264
14265# DOM diff for a single replay-diff screenshot
14266meticulous agent dom-diff --replayDiffId="<id>" --screenshotName="<name>"
14267
14268# Timeline diff for a replay diff
14269meticulous agent timeline-diff --replayDiffId="<id>"
14270\`\`\`
14271
14272---
14273
14274### Review diffs
14275
14276\`\`\`bash
14277meticulous agent diff-comments --replayDiffId="<id>" --screenshotName="<name>"
14278meticulous agent diff-comments ... --includeResolved  # include resolved comments
14279meticulous agent reject-diff ... --reason="..." --x=0.5 --y=0.5
14280meticulous agent ignore-diff ... --reason="..." --x=0.5 --y=0.5
14281meticulous agent create-diff-comment ... --text="..." --x=0.4 --y=0.6
14282meticulous agent reply-to-diff-comment --commentId="<id>" --text="..."
14283\`\`\`
14284
14285---
14286
14287### Retrieve a non-visual check report
14288
14289\`\`\`bash
14290meticulous agent test-run-check --checkId="accessibility"  # builtin check for the current commit
14291meticulous agent test-run-check --checkId="network-requests" --testRunId="<id>"  # …or an explicit run
14292meticulous agent test-run-check --checkId="accessibility" --commitSha="<sh
14292a>"  # …or a specific commit
14293meticulous agent test-run-check --checkType="custom" --checkId="my-check"  # customer-reported check
14294
14295# List available check IDs
14296meticulous agent test-run-check --availableIds  # for the current commit
14297meticulous agent test-run-check --availableIds --testRunId="<id>"  # …or an explicit run
14298meticulous agent test-run-check --availableIds --commitSha="<sha>"  # …or a specific commit
14299\`\`\`
14300
14301---
14302
14303### Inspect JS code coverage
14304
14305\`\`\`bash
14306# Per-file coverage: a test run, the project's latest run, or a single replay
14307meticulous agent js-coverage                           # coverage for current commit
14308meticulous agent js-coverage --testRunId="<id>"        # …or for an explicit test run
14309meticulous agent js-coverage --commitSha="<sha>"       # …or for a specific commit
14310meticulous agent js-coverage --dontWaitForTestRunToComplete  # don't block on in-progress runs
14311meticulous agent js-coverage --latestForProject        # …or project's preferred latest successful run
14312meticulous agent js-coverage --project="{org}/{proj}"  # override default project for this call
14313meticulous agent js-coverage --replayId="<id>"         # coverage for a single replay
14314meticulous agent js-coverage --replayId="<id>" --screenshotName="<name>"  # or a single screenshot
14315meticulous agent js-coverage --summary                 # aggregate totals, not the per-file list
14316
14317# Coverage diff: a whole test run against its own base run, or one replay diff
14318meticulous agent js-coverage-diff                        # diff for the current commit's run
14319meticulous agent js-coverage-diff --testRunId="<id>"     # …or for an explicit test run
14320meticulous agent js-coverage-diff --commitSha="<sha>"    # …or for a specific commit
14321meticulous agent js-coverage-diff --replayDiffId="<id>"  # …or the diff of a single replay pair
14322meticulous agent js-coverage-diff --replayDiffId="<id>" --screenshotName="<name>"
14323meticulous agent js-coverage-diff --summary              # aggregate difference (whole-run only)
14324
14325# Filter and page — these work the same on js-coverage and js-coverage-diff
14326meticulous agent js-coverage --globFilter="src/components/**"  # only matching repo paths
14327meticulous agent js-coverage --globFilter="src/**" --globFilter="libs/**"  # any of several
14328meticulous agent js-coverage --limit=50                        # page size (1-1000, default 100)
14329meticulous agent js-coverage --limit=100 --offset=100          # the next page
14330
14331# Order, choose columns, and pick rows (js-coverage on a whole test run only)
14332meticulous agent js-coverage --orderBy=uncoveredLines --limit=50  # the 50 least-covered files
14333meticulous agent js-coverage --orderBy=coveragePercentage --order=asc
14334meticulous agent js-coverage --includeExecutedRanges      # executed line ranges (default)
14335meticulous agent js-coverage --includeExecutableRanges    # line ranges that could be executed
14336meticulous agent js-coverage --includeUncoveredRanges     # executable ranges that were not executed
14337meticulous agent js-coverage --includeLineCounts          # executed/executable/uncovered line counts
14338meticulous agent js-coverage --includeCoveragePercentage  # % of executable lines executed
14339meticulous agent js-coverage --includeAllFiles            # not just ones with coverage
14340meticulous agent js-coverage --prDiffOnly                 # restrict to files changed in the PR diff
14341
14342# Aggregated coverage for multiple test runs (same project + commit; test-run only)
14343meticulous agent js-coverage --headPlusTestRunIds="<id1>,<id2>"  # in addition to the current commit
14344meticulous agent js-coverage --testRunIds="<id1>,<id2>,<id3>"    # list of runs to combine
14345
14346# Complete a base run
14347meticulous agent complete-base-run                        # replay the rest of the current commit's base run
14348meticulous agent complete-base-run --testRunId="<id>"     # …or of an explicit run
14349meticulous agent complete-base-run --commitSha="<sha>"    # …or of a specific commit's run
14350meticulous agent complete-base-run --project="{org}/{proj}"  # override default project for this call
14351meticulous agent complete-base-run --dontWaitForTestRunToComplete  # return once the replays are scheduled
14352\`\`\`
14353
14354---
14355
14356### Find recently created sessions
14357
14358\`\`\`bash
14359meticulous agent sessions                               # 100 most recently created sessions
14360meticulous agent sessions --project="{org}/{proj}"      # override default project for this call
14361meticulous agent sessions --createdSince="2026-07-01"   # only sessions created at/after this date
14362meticulous agent sessions --recordedSince="2026-07-01"  # only sessions originally recorded at/after
14363meticulous agent sessions --recordedBy="[email protected]"  # only sessions recorded by this identity
14364meticulous agent sessions --excludeSyntheticSessions    # drop patched/sliced/mutated sessions
14365meticulous agent sessions --visitedUrlFilter="*/checkout*"  # only sessions that visited a matching URL
14366meticulous agent sessions --selectedSet                 # only sessions in the current selected set
14367meticulous agent sessions --selectedSet="2026-07-01"    # only sessions selected as of that point
14368meticulous agent sessions --selectedSet --includeSelectedSince  # add when each entered the set
14369meticulous agent sessions --selectedSet --orderBy=rank  # in the selection's own pick order
14370meticulous agent sessions --selectedSet --orderBy=selectedSince  # most recently selected first
14371meticulous agent sessions --selectedSet --includeAdditionalCoverage  # add the coverage each one added
14372meticulous agent sessions --selectedSet --orderBy=additionalCoverage  # biggest contributors first
14373meticulous agent sessions --order=asc                   # reverse the chosen ordering
14374meticulous agent sessions --includeDurationSeconds      # add a durationSeconds column
14375meticulous agent sessions --includeNumberUserEvents     # add a numberUserEvents column
14376meticulous agent sessions --includeNumberUrlsVisited    # add a numberUrlsVisited column
14377meticulous agent sessions --includeStartUrl             # add a startUrl column
14378meticulous agent sessions --includeAbandonedReason      # add an abandonedReason column
14379meticulous agent sessions --limit=25 --offset=50        # override count / page through results
14380
14381# Identify sessions that exercise your branch's code changes
14382meticulous local relevant-sessions --format=multi-file --minimum-times-to-cover-each-line=1
14383\`\`\`
14384
14385---
14386
14387### Export reporting statistics
14388
14389\`\`\`bash
14390meticulous agent test-run-stats --since="2026-08-01"        # --until defaults to now
14391meticulous agent test-run-stats --testRunIds="<id1>,<id2>"  # or scope by identifiers instead
14392meticulous agent test-run-stats --prNumbers="123,456"
14393meticulous agent test-run-stats --commitShas="<sha1>,<sha2>"
14394meticulous agent project-daily-stats --since="2026-08-01"   # matches the Metrics dashboard
14395meticulous agent test-run-event-stats --since="2026-08-01"  # views, reviews, comments
14396meticulous agent test-run-event-stats --eventTypes="test_run_viewed" --since="2026-08-01"
14397meticulous agent test-run-event-stats --cursor="<next-cursor>" --since="2026-08-01"  # next page
14398\`\`\`
14399
14400---
14401
14402### Upload a build and trigger a test run
14403
14404\`\`\`bash
14405# Upload a static build, or a container image, and capture the deploymentId
14406meticulous agent upload-build --appDirectory="<path-to-build>"
14407meticulous agent upload-build --localImageTag="<image-tag>" --commitSha="<sha>"
14408
14409# Trigger a run against that deployment (infers a diff against local HEAD)
14410meticulous agent trigger-test-run --deploymentId="<id>"
14411meticulous agent trigger-test-run --project="{org}/{proj}"  # override default project for this call
14412
14413# …and pin an explicit base (diffs against local HEAD)
14414meticulous agent trigger-test-run --deploymentId="<id>" --baseSha="<sha>"
14415
14416# …or pass a diff yourself, instead of inferring one locally
14417meticulous agent trigger-test-run --deploymentId="<id>" --baseSha="<sha>" --gitDiffOutput="<diff>"
14418
14419# …or skip the upload step and target an already-uploaded deployment for a commit
14420meticulous agent trigger-test-run
14421meticulous agent trigger-test-run --commitSha="<sha>"
14422
14423# Replay only specific sessions, instead of the auto-selected golden set
14424meticulous agent trigger-test-run --sessionIds="<id1>,<id2>"
14425\`\`\`
14426
14427---
14428
14429### Submit feedback about Meticulous
14430
14431\`\`\`bash
14432# Tell the Meticulous team whether Meticulous helped, and what would have made your task easier
14433meticulous agent submit-feedback --message="<one or two sentences>"
14434meticulous agent submit-feedback --message="<…>" --outcome="helped"       # or "neutral" / "hindered"
14435meticulous agent submit-feedback --message="<…>" --testRunId="<id>"       # tie it to a test run
14436meticulous agent submit-feedback --message="<…>" --skill="meticulous-review"  # workflow being followed
14437meticulous agent submit-feedback --message="<…>" --agentName="claude-code" --agentModel="<model>"
14438\`\`\`
14439
14440---
14441
14442### Set up an AI-ready debug workspace
14443
14444\`\`\`bash
14445# Download all replay data into a structured local debug workspace in ~/.meticulous
14446meticulous debug replay <replayId>           # debug a single replay, optionally with --baseReplayId
14447meticulous debug replay-diff <replayDiffId>  # debug a specific replay diff
14448meticulous debug clean                       # clean up old debug workspaces
14449\`\`\`
14450
14451---
14452
14453### Run Agent swarm (beta)
14454
14455Agent 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.
14456
14457\`\`\`bash
14458meticulous ci agent-test \\
14459  --assetsDir="build" \\
14460  --commitSha="<pr-head-sha>" \\
14461  --instructionsFile=".github/agent-review/instructions.md"
14462\`\`\`
14463
14464Use 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\`.
14465
14466For 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_REVIEW_DOCS_URL}) for the prerequisites, CI workflow, and staging-backend setup.
14467
14468---
14469
14470### Replay a single session locally
14471
14472\`\`\`bash
14473meticulous simulate --sessionId="<id>" --appUrl="<appUrl>"
14474meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --baseReplayId="<id>"  # diff against base
14475meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --screenshot  # capture screenshots
14476meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --headless    # run in headless mode
14477meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --devtools    # open Chromium DevTools
14478meticulous simulate --sessionId="<id>" --appUrl="<appUrl>" --maxDurationMs=<ms>  # set max virtual time
14479\`\`\`
14480
14481---
14482
14483### Download artefacts
14484
14485\`\`\`bash
14486# Downloads to ~/.meticulous/ by default (override with --dataDir)
14487meticulous download session --sessionId="<id>"
14488meticulous download replay --replayId="<id>"
14489meticulous download test-run --testRunId="<id>"
14490\`\`\`
14491`},{id:"agents-mcp-server",url:"/docs/agents/mcp-server",document:`---
14492{
14493  "title": "MCP server for agents"
14494}
14495---
14496
14497# {% $frontmatter.title %}
14498
14499The 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.
14500
14501- [What it's for](#what-its-for)
14502- [Connecting a client](#connecting-a-client)
14503- [Authenticating with a token instead](#authenticating-with-a-token-instead)
14504- [Available tools](#available-tools)
14505- [What this connector can access](#what-this-connector-can-access)
14506- [Troubleshooting](#troubleshooting)
14507- [Support and policies](#support-and-policies)
14508
14509---
14510
14511## What it's for
14512
14513Meticulous 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.
14514
14515---
14516
14517## Connecting a client
14518
14519The 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.
14520
14521{% expand title="OAuth details for other clients" %}
14522
14523Discovery 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.
14524
14525- **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.
14526- **Without registration:** client id \`meticulous-cli\`, public client with PKCE (S256), scopes \`openid email profile offline_access\`.
14527- **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.
14528
14529{% /expand %}
14530
14531**Claude Code**
14532
14533Run the following in your terminal:
14534
14535\`\`\`bash
14536claude mcp add --transport http Meticulous https://app.meticulous.ai/api/mcp
14537\`\`\`
14538
14539Then, in Claude Code, type \`/mcp\` and choose "Authenticate" for the Meticulous MCP.
14540
14541**Cursor**
14542
14543Add to \`~/.cursor/mcp.json\` (global) or \`.cursor/mcp.json\` (per project):
14544
14545\`\`\`json
14546{
14547  "mcpServers": {
14548    "Meticulous": { "url": "https://app.meticulous.ai/api/mcp" }
14549  }
14550}
14551\`\`\`
14552
14553**Codex/ChatGPT**
14554
14555Add a server with name "Meticulous" and URL \`https://app.meticulous.ai/api/mcp\`, then click "Authenticate".
14556
14557Calls 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\`.
14558
14559---
14560
14561## Authenticating with a token instead
14562
14563Any 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.
14564
14565\`\`\`json
14566{
14567  "url": "https://app.meticulous.ai/api/mcp",
14568  "headers": { "Authorization": "Bearer <token>" }
14569}
14570\`\`\`
14571
14572---
14573
14574## Available tools
14575
14576Every 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.
14577
14578### Identity and project selection
14579
14580| Tool | Access | What it does |
14581|---|---|---|
14582| \`whoami\` | Read | Show the identity the connection is authenticated as, and the project it resolves to. |
14583| \`list_projects\` | Read | List the projects the authenticated user, or API token, can access. |
14584| \`get_project\` | Read | Show the project project-scoped tools use when not given a \`project\` argument, and where that came from. |
14585| \`set_project\` | Write | Change the default project for the user account — every session and machine, not just this connection. |
14586
14587### Test runs and diffs
14588
14589| Tool | Access | What it does |
14590|---|---|---|
14591| \`get_test_run_for_commit\` | Read | Look up the latest test run for a commit; returns its ID and status. |
14592| \`get_test_run_diffs\` | Read | Get the (curated, full, or Similar-group) list of screenshot diffs for a test run. |
14593| \`get_test_run_diffs_counts\` | Read | Get aggregate diff counts for a test run, including the six-way review-decision breakdown. |
14594| \`get_image_urls\` | Read | Get signed URLs for a screenshot diff's before/after/diff images. |
14595| \`get_images\` | Read | Get a screenshot diff's before, after, and diff images as native MCP image blocks. |
14596| \`get_dom_diff\` | Read | Get the structural DOM diff for one screenshot diff, as unified-diff-style hunks. |
14597| \`get_timeline_diff\` | Read | Get the list of timeline event differences (e.g. network requests, DOM mutations) for a replay diff. |
14598| \`get_test_run_check\` | Read | Get the Markdown report for a builtin or custom non-visual check. |
14599| \`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. |
14600
14601#### Results that are not ready yet
14602
14603Every 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.
14604
14605For \`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.
14606
14607### Reviewing diffs
14608
14609| Tool | Access | What it does |
14610|---|---|---|
14611| \`get_diff_comments\` | Read | Get the review comments (with replies) for a screenshot diff, oldest first. |
14612| \`reject_diff\` | Write | Agent-reject a screenshot diff and comment why. |
14613| \`ignore_diff\` | Write | Agent-ignore a screenshot diff as unrelated to the change under review, and comment why. |
14614| \`create_diff_comment\` | Write | Start a review comment thread at approximate image coordinates. |
14615| \`reply_to_diff_comment\` | Write | Reply to an existing review comment thread. |
14616
14617### JS coverage
14618
14619| Tool | Access | What it does |
14620|---|---|---|
14621| \`get_test_run_js_coverage\` | Read | Get per-file JavaScript coverage for a test run. |
14622| \`get_project_js_coverage\` | Read | Get per-file JavaScript coverage for a project's latest successful test run. |
14623| \`get_test_run_js_coverage_summary\` | Read | Get aggregate JavaScript coverage totals for a test run. |
14624| \`get_project_js_coverage_summary\` | Read | Get aggregate JavaScript coverage totals for a project's latest successful test run. |
14625| \`get_replay_js_coverage\` | Read | Get per-file JavaScript coverage for a single replay, or one screenshot of it. |
14626| \`get_test_run_js_coverage_diff\` | Read | Get per-file JavaScript coverage differences for a test run against its own base run. |
14627| \`get_test_run_js_coverage_diff_summary\` | Read | Get the aggregate JavaScript coverage difference for a test run against its own base run. |
14628| \`get_replay_diff_js_coverage_diff\` | Read | Get per-file JavaScript coverage differences (base vs. head) for a replay diff. |
14629
14630### Sessions
14631
14632| Tool | Access | What it does |
14633|---|---|---|
14634| \`get_sessions\` | Read | List a project's recorded sessions, newest first by default, optionally narrowed to the selected set. |
14635| \`get_session_data\` | Read | Get the recorded user-flow and network summary for a session — useful for understanding what a replay exercises. |
14636
14637### Reporting statistics
14638
14639| Tool | Access | What it does |
14640|---|---|---|
14641| \`get_test_run_stats\` | Read | Export reporting statistics for a project's test runs. |
14642| \`get_project_daily_stats\` | Read | Export daily project reporting statistics matching the Metrics dashboard. |
14643| \`get_test_run_event_stats\` | Read | Export a project's test-run reporting events such as views, reviews, and comments. |
14644
14645### Triggering a test run
14646
14647| Tool | Access | What it does |
14648|---|---|---|
14649| \`request_asset_upload\` | Write | Request a signed URL to upload a zipped static-asset build. |
14650| \`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. |
14651| \`request_container_upload\` | Write | Request registry credentials to push a Docker container build. |
14652| \`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. |
14653| \`trigger_test_run\` | Write | Trigger a test run against a registered deploymentId, or a persistent CI deployment identified by commit SHA. |
14654| \`complete_base_run\` | Write | Replay the selected sessions a base run has not run yet. |
14655
14656### Feedback
14657
14658| Tool | Access | What it does |
14659|---|---|---|
14660| \`submit_feedback\` | Write | Send free-form feedback about Meticulous to the Meticulous team. |
14661
14662---
14663
14664## What this connector can access
14665
14666- **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).
14667- **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.
14668- **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
14668de.
14669- **Scope:** every call is scoped to the projects the authenticated user (or, for a project API token, the single owning project) has access to.
14670
14671---
14672
14673## Troubleshooting
14674
14675- **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.
14676- **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.
14677- **"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).
14678- **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.
14679- **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.
14680- Anything else: contact us (see below).
14681
14682---
14683
14684## Support and policies
14685
14686- **Support:** [${p.METICULOUS_SUPPORT_EMAIL}](mailto:${p.METICULOUS_SUPPORT_EMAIL})
14687- **Privacy policy:** [meticulous.ai/privacy-policy](https://www.meticulous.ai/privacy-policy)
14688- **Terms of service:** [meticulous.ai/terms-conditions](https://www.meticulous.ai/terms-conditions)
14689- **Security and compliance:** [security.meticulous.ai](https://security.meticulous.ai/)
14690`},{id:"agents-skills",url:"/docs/agents/skills",document:`---
14691{
14692  "title": "Skills & Use cases"
14693}
14694---
14695
14696# {% $frontmatter.title %}
14697
14698Skills 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.
14699
14700- [Use cases](#use-cases)
14701  - [Ask the agent to review your test-run diffs](#ask-the-agent-to-review-your-test-run-diffs)
14702  - [Ask the agent to fix your rejected diffs](#ask-the-agent-to-fix-your-rejected-diffs)
14703  - [Ask the agent to implement a change end-to-end against Meticulous](#ask-the-agent-to-implement-a-change-end-to-end-against-meticulous)
14704  - [Ask the agent to increase coverage for your project](#ask-the-agent-to-increase-coverage-for-your-project)
14705- [Why reject and comment?](#why-reject-and-comment)
14706- [Install & update the skills](#install-update-the-skills)
14707- [Skills reference](#skills-reference)
14708
14709---
14710
14711## Use cases
14712
14713There 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.
14714
14715### Ask the agent to review your test-run diffs
14716
14717> *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.*
14718
14719The [\`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.
14720
14721If 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.
14722
14723### Ask the agent to fix your rejected diffs
14724
14725> *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.*
14726
14727The [\`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.
14728
14729### Ask the agent to implement a change end-to-end against Meticulous
14730
14731> *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.*
14732
14733The 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.
14734
14735It 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.
14736
14737### Ask the agent to increase coverage for your project
14738
14739> *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.*
14740
14741The [\`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.
14742
14743Two 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.
14744
14745---
14746
14747## Why reject and comment? {% #why-reject-and-comment %}
14748
14749Rejecting a diff and pinning a comment on it isn't just bookkeeping — it's the task list your agent works from.
14750
14751- **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.
14752- **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.
14753- **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.
14754- **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.
14755
14756Because 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\`.
14757
14758---
14759
14760## Install & update the skills
14761
14762To install, and update, the skills into your project using [npx skills](https://github.com/vercel-labs/skills) (for the specified agents):
14763
14764\`\`\`bash
14765npx skills add alwaysmeticulous/skills --skill "*" --agent claude-code --agent codex --agent cursor -y
14766\`\`\`
14767
14768---
14769
14770## Skills reference
14771
14772#### meticulous-review
14773
14774Analyze 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.
14775
14776#### meticulous-fix
14777
14778Fix 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.
14779
14780#### meticulous-test
14781
14782Run 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
14782nd before creating a PR.
14783
14784#### meticulous-zero-diff-task
14785
14786Implement 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.
14787
14788#### meticulous-increase-coverage
14789
14790Increase 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.
14791
14792#### meticulous-iterative-dev
14793
14794Iterative 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.
14795
14796#### meticulous-simulate-and-diff
14797
14798Run 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.
14799
14800#### meticulous-use-session-data
14801
14802Download 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
14802overs, or when you want network mocks for writing tests.
14803
14804---
14805
14806### Supporting skills
14807
14808#### meticulous-cli
14809
14810Overview 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.
14811
14812#### meticulous-cli-update
14813
14814Checks 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.
14815`},{id:"cli-commands",url:"/docs/reference/cli-commands",document:`---
14816{
14817  "title": "CLI Commands Reference"
14818}
14819---
14820
14821# {% $frontmatter.title %}
14822
14823Complete reference for Meticulous CLI commands, flags, and usage patterns.
14824
14825---
14826
14827## Installation
14828
14829Install the CLI globally or use npx:
14830
14831\`\`\`bash
14832# Using npx (recommended)
14833npx @alwaysmeticulous/cli [command]
14834
14835# Or install globally
14836npm install -g @alwaysmeticulous/cli
14837meticulous [command]
14838\`\`\`
14839
14840---
14841
14842## Commands Overview
14843
14844| Command | Purpose | Use Case |
14845|---------|---------|----------|
14846| \`onboard\` | Install Meticulous using local Claude Code or Codex | Initial recorder and CI setup |
14847| \`ci run-with-tunnel\` | Run tests in cloud via tunnel | CI testing with local app |
14848| \`ci upload-assets\` | Upload and test static assets | CI testing for static sites |
14849| \`ci upload-asset-chunk\` | Upload one named, versioned asset chunk | Multi-bundle deployments |
14850| \`ci run-with-uploaded-asset-chunks\` | Trigger a test run against uploaded chunks | Multi-bundle deployments |
14851| \`ci upload-container\` | Upload Docker container and test | CI testing with containers |
14852| \`ci agent-test\` | Upload a build and launch an agent to explore the PR | Beta, opt-in Agent swarm |
14853| \`ci run-local\` | Run all replay test cases locally | Local test execution |
14854| \`ci prepare\` | Ensure base run exists | CI setup |
14855| \`ci label-commit\` | Attach labels to a commit | Marking commits as not relevant for testing |
14856| \`ci start-tunnel\` | Start secure tunnel | Manual testing/debugging |
14857| \`simulate\` (alias: \`replay\`) | Replay session locally | Local debugging |
14858| \`record session\` | Record a user session | Session recording |
14859| \`record login\` | Record a login flow | Login flow recording |
14860| \`crawl\` | Crawl your app to record sessions and create a test run | Bootstrapping session coverage |
14861| \`auth login\` | Force a fresh browser login and select a project | Authentication |
14862| \`auth whoami\` | Show current user | Authentication check |
14863| \`auth logout\` | Revoke and clear stored tokens | Authentication |
14864| \`auth get-project\` | Print your default project | Authentication |
14865| \`auth set-project\` | Choose your default project | Authentication |
14866| \`auth list-projects\` | List the projects you can access | Authentication |
14867| \`project show\` | Show linked project | Project info |
14868| \`project upload-source\` | Upload a source-code archive for a given commit | Source coverage / CI |
14869| \`download session\` | Download a recorded session | Debugging |
14870| \`download replay\` | Download a replay | Debugging |
14871| \`download test-run\` | Download a test run | Debugging |
14872| \`local relevant-sessions\` | Find sessions covering the current branch's code changes | Local development |
14873| \`debug replay\` | Set up a debug workspace for a single replay | Investigating a replay |
14874| \`debug replay-diff\` | Set up a debug workspace for a specific replay diff | Investigating a diff |
14875| \`debug clean\` | Clean up debug workspaces | Debug workspace maintenance |
14876| \`agent upload-build\` | Upload a build (static assets or container) and capture a deployment ID | Agent/programmatic use |
14877| \`agent trigger-test-run\` | Trigger a test run against an uploaded build | Agent/programmatic use |
14878| \`agent test-run-diffs\` | List replay diffs for a test run with summary | Agent/programmatic use |
14879| \`agent diff-comments\` | Get review comments for a replay-diff screenshot | Agent/programmatic use |
14880| \`agent reject-diff\` | Agent-reject a screenshot diff and comment why | Agent/programmatic use |
14881| \`agent ignore-diff\` | Agent-ignore a screenshot diff as unrelated to the change and comment why | Agent/programmatic use |
14882| \`agent create-diff-comment\` | Start a review comment thread on a screenshot diff | Agent/programmatic use |
14883| \`agent reply-to-diff-comment\` | Reply to a review comment thread | Agent/programmatic use |
14884| \`agent dom-diff\` | Get the DOM diff for a replay-diff screenshot | Agent/programmatic use |
14885| \`agent image-urls\` | Get screenshot image URLs for a replay-diff screenshot | Agent/programmatic use |
14886| \`agent image-files\` | Download screenshot images to \`~/.meticulous/agent-images\` | Agent/programmatic use |
14887| \`agent timeline-diff\` | Get the timeline diff for a replay diff | Agent/programmatic use |
14888| \`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 |
14889| \`agent test-run-for-commit\` | Look up the latest test run for a commit (defaults to git HEAD) | Agent/programmatic use |
14890| \`agent sessions\` | List a project's recorded sessions, newest first by default, optionally narrowed to the selected set | Agent/programmatic use |
14891| \`agent test-run-stats\` | Export reporting statistics for a project's test runs | Agent/programmatic use |
14892| \`agent project-daily-stats\` | Export daily project reporting statistics | Agent/programmatic use |
14893| \`agent test-run-event-stats\` | Export a project's test-run reporting events | Agent/programmatic use |
14894| \`agent js-coverage\` | Get JS coverage for a replay or a whole test run, per file or as aggregate totals | Agent/programmatic use |
14895| \`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 |
14896| \`agent upload-build\` | Upload a build (static assets or container) and capture a deployment ID | Agent/programmatic use |
14897| \`agent trigger-test-run\` | Trigger a test run against an uploaded build | Agent/programmatic use |
14898| \`agent complete-base-run\` | Replay the selected sessions a base run has not run yet | Agent/programmatic use |
14899| \`agent submit-feedback\` | Submit free-form feedback about Meticulous to the Meticulous team | Agent/programmatic use |
14900| \`schema\` | Print the CLI command schema as JSON | Agent/programmatic use |
14901
14902For 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}).
14903
14904---
14905
14906## onboard
14907
14908Install Meticulous in the current Git repository using your local Claude Code
14909or Codex. The command reviews the frontend application and prepares a pull
14910request with recorder and CI configuration. Model inference runs through your
14911own coding-agent account, not Meticulous-hosted inference.
14912
14913### Authentication
14914
14915If you are not already logged in, \`onboard\` opens a browser to sign in and
14916then selects the Meticulous project. On a remote or sandboxed machine, run
14917\`npx @alwaysmeticulous/cli auth login --device\` first. Alternatively, pass
14918\`--apiToken\`.
14919
14920### Examples
14921
14922\`\`\`bash
14923# Interactive setup from the connected application repository
14924npx @alwaysmeticulous/cli onboard --project="<ORGANIZATION>/<PROJECT>"
14925
14926# Choose an app in a monorepo and use Claude Code
14927npx @alwaysmeticulous/cli onboard \\
14928  --project="<ORGANIZATION>/<PROJECT>" \\
14929  --app="apps/web" \\
14930  --agent=claude
14931
14932# Prepare the workspace without launching an agent
14933npx @alwaysmeticulous/cli onboard --printOnly
14934\`\`\`
14935
14936Key options include \`--cwd\`, \`--project\`, \`--app\`, \`--agent\`,
14937\`--model\`, \`--headless\`, \`--auto\`, \`--printOnly\`, and
14938\`--apiToken\`.
14939
14940---
14941
14942## ci run-with-tunnel
14943
14944Run Meticulous tests in the cloud against a locally-running application.
14945
14946### Synopsis
14947
14948\`\`\`bash
14949npx @alwaysmeticulous/cli ci run-with-tunnel \\
14950  --apiToken="<token>" \\
14951  --appUrl="<url>" \\
14952  [options]
14953\`\`\`
14954
14955### Required Flags
14956
14957#### \`--apiToken\`
14958
14959**Type**: String
14960**Description**: Your Meticulous API token
14961**How to get**: From Meticulous dashboard project settings
14962
14963**Example**:
14964\`\`\`bash
14965--apiToken="met_live_abc123..."
14966\`\`\`
14967
14968**Note**: Can also be set via \`METICULOUS_API_TOKEN\` environment variable.
14969
14970---
14971
14972#### \`--appUrl\`
14973
14974**Type**: String
14975**Description**: URL where your app is running
14976**Format**: Full URL including protocol and port
14977
14978**Examples**:
14979\`\`\`bash
14980--appUrl="http://localhost:3000"
14981--appUrl="http://localhost:8080"
14982--appUrl="https://localhost:3000"
14983\`\`\`
14984
14985---
14986
14987### Optional Flags
14988
14989#### \`--commitSha\`
14990
14991**Type**: String
14992**Description**: Commit SHA being tested
14993**Default**: Auto-detected from git
14994
14995**Example**:
14996\`\`\`bash
14997--commitSha="$GITHUB_SHA"
14998--commitSha="abc123def456..."
14999\`\`\`
15000
15001---
15002
15003#### \`--companionAssetsFolder\`
15004
15005**Type**: String (path)
15006**Description**: Path to local folder with static assets to upload
15007**Default**: None
15008**Requires**: Must also provide \`--companionAssetsRegex\`
15009
15010**Example**:
15011\`\`\`bash
15012--companionAssetsFolder="companion-assets"
15013\`\`\`
15014
15015---
15016
15017#### \`--companionAssetsRegex\`
15018
15019**Type**: String (regex)
15020**Description**: Regex pattern for requests to serve from companion assets
15021**Default**: None
15022**Requires**: Must also provide \`--companionAssetsFolder\`
15023
15024**Example**:
15025\`\`\`bash
15026--companionAssetsRegex="^/_next/static/"
15027\`\`\`
15028
15029---
15030
15031#### \`--proxyAllUrls\`
15032
15033**Type**: Boolean
15034**Description**: Proxy all URLs through tunnel (not just app URL)
15035**Default**: false
15036
15037**Example**:
15038\`\`\`bash
15039--proxyAllUrls
15040\`\`\`
15041
15042**Use case**: Multi-server applications (frontend + API on different ports)
15043
15044---
15045
15046#### \`--rewriteHostnameToAppUrl\`
15047
15048**Type**: Boolean
15049**Description**: Rewrite request hostname to match app U
15049RL
15050**Default**: false
15051
15052**Example**:
15053\`\`\`bash
15054--rewriteHostnameToAppUrl
15055\`\`\`
15056
15057**Use case**: When HTML contains absolute URLs
15058
15059---
15060
15061#### \`--secureTunnelHost\`
15062
15063**Type**: String
15064**Description**: Custom tunnel server host
15065**Default**: Meticulous production tunnel
15066**Note**: For Meticulous team use only
15067
15068---
15069
15070#### \`--hadPreparedForTests\`
15071
15072**Type**: Boolean
15073**Description**: Indicate that \`meticulous ci prepare\` was already run
15074**Default**: false
15075
15076---
15077
15078### Complete Example
15079
15080\`\`\`bash
15081# Basic usage
15082npx @alwaysmeticulous/cli ci run-with-tunnel \\
15083  --apiToken="$METICULOUS_API_TOKEN" \\
15084  --appUrl="http://localhost:3000"
15085
15086# With companion assets (Next.js)
15087npx @alwaysmeticulous/cli ci run-with-tunnel \\
15088  --apiToken="$METICULOUS_API_TOKEN" \\
15089  --appUrl="http://localhost:3000" \\
15090  --companionAssetsFolder="companion-assets" \\
15091  --companionAssetsRegex="^/_next/static/"
15092
15093# Multi-server app
15094npx @alwaysmeticulous/cli ci run-with-tunnel \\
15095  --apiToken="$METICULOUS_API_TOKEN" \\
15096  --appUrl="http://localhost:3000" \\
15097  --proxyAllUrls
15098\`\`\`
15099
15100---
15101
15102### Exit Codes
15103
15104| Code | Meaning |
15105|------|---------|
15106| 0 | Success - all tests passed or diffs approved |
15107| 1 | Failure - tests failed or unapproved diffs |
15108| 2 | Error - configuration or connection error |
15109
15110---
15111
15112## ci agent-test
15113
15114{% callout type="info" title="Beta opt-in" %}
15115Agent 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_REVIEW_DOCS_URL}) explains how to request access and configure the workflow.
15116{% /callout %}
15117
15118Upload one build target and launch a Meticulous-hosted agent that explores the pull request and creates additional recorded sessions.
15119
15120### Synopsis
15121
15122\`\`\`bash
15123npx @alwaysmeticulous/cli ci agent-test \\
15124  --assetsDir="<path-to-built-assets>" \\
15125  --commitSha="<pr-head-sha>" \\
15126  [options]
15127\`\`\`
15128
15129Provide exactly one target:
15130
15131- \`--assetsDir\` — a built static frontend directory.
15132- \`--localImageTag\` — a locally built Docker image.
15133
15134Use \`--instructionsFile\` to give the agent routes and flows to exercise. 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.
15135
15136For 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.
15137
15138---
15139
15140## ci upload-assets
15141
15142Upload static assets and run tests in the cloud.
15143
15144### Synopsis
15145
15146\`\`\`bash
15147npx @alwaysmeticulous/cli ci upload-assets \\
15148  --apiToken="<token>" \\
15149  --appDirectory="<path>" \\
15150  [options]
15151\`\`\`
15152
15153### Required Flags
15154
15155#### \`--apiToken\`
15156
15157**Type**: String
15158**Description**: Your Meticulous API token
15159
15160---
15161
15162#### \`--appDirectory\`
15163
15164**Type**: String (path)
15165**Description**: Path to directory containing built static assets
15166**Common values**: \`dist\`, \`build\`, \`out\`
15167
15168**Examples**:
15169\`\`\`bash
15170--appDirectory="dist"        # Vite
15171--appDirectory="build"       # Create React App
15172--appDirectory="out"         # Next.js static export
15173\`\`\`
15174
15175---
15176
15177### Optional Flags
15178
15179#### \`--commitSha\`
15180
15181**Type**: String
15182**Description**: Commit SHA being tested
15183**Default**: Auto-detected from git
15184
15185---
15186
15187#### \`--rewrites\`
15188
15189**Type**: String (JSON)
15190**Description**: URL rewrite rules in Vercel format
15191**Use case**: SPA routing, redirects
15192
15193**Example**:
15194\`\`\`bash
15195--rewrites='[{"source":"/(.*)", "destination":"/index.html"}]'
15196\`\`\`
15197
15198**Common patterns**:
15199
15200**SPA routing**:
15201\`\`\`json
15202[{"source": "/(.*)", "destination": "/index.html"}]
15203\`\`\`
15204
15205**API proxy**:
15206\`\`\`json
15207[{"source": "/api/(.*)", "destination": "https://api.example.com/$1"}]
15208\`\`\`
15209
15210---
15211
15212#### \`--waitForBase\`
15213
15214**Type**: Boolean
15215**Description**: Wait for base test run
15216**Default**: false
15217
15218---
15219
15220#### \`--waitForTestRunToComplete\`
15221
15222**Type**: Boolean
15223**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.
15224
15225**Default**: false (omit the flag)
15226
15227**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\` (
15227or \`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.
15228
15229**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."
15230
15231**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.
15232
15233---
15234
15235#### \`--json\`
15236
15237**Type**: Boolean
15238**Description**: Print one machine-readable JSON result on stdout. See [Machine-readable output](#machine-readable-output).
15239**Default**: \`false\`
15240
15241---
15242
15243### Complete Example
15244
15245\`\`\`bash
15246# Basic usage
15247npx @alwaysmeticulous/cli ci upload-assets \\
15248  --apiToken="$METICULOUS_API_TOKEN" \\
15249  --appDirectory="dist"
15250
15251# With SPA routing
15252npx @alwaysmeticulous/cli ci upload-assets \\
15253  --apiToken="$METICULOUS_API_TOKEN" \\
15254  --appDirectory="dist" \\
15255  --rewrites='[{"source":"/(.*)", "destination":"/index.html"}]'
15256
15257# With commit SHA
15258npx @alwaysmeticulous/cli ci upload-assets \\
15259  --apiToken="$METICULOUS_API_TOKEN" \\
15260  --appDirectory="build" \\
15261  --commitSha="$CI_COMMIT_SHA"
15262\`\`\`
15263
15264---
15265
15266### Exit Codes
15267
15268| Code | Meaning |
15269|------|---------|
15270| 0 | Success |
15271| 1 | Failure |
15272| 2 | Error |
15273
15274---
15275
15276## ci upload-asset-chunk
15277
15278Upload a named, versioned chunk of static assets to Meticulous for incremental deployments.
15279
15280### Synopsis
15281
15282\`\`\`bash
15283npx @alwaysmeticulous/cli ci upload-asset-chunk \\
15284  --apiToken="<token>" \\
15285  --chunkName="<name>" \\
15286  --chunkVersionId="<version>" \\
15287  --chunkAssetsDirectory="<path>" \\
15288  [options]
15289\`\`\`
15290
15291### Required Flags
15292
15293#### \`--apiToken\`
15294
15295**Type**: String
15296**Description**: Your Meticulous API token
15297
15298**Note**: Can also be set via \`METICULOUS_API_TOKEN\` environment variable.
15299
15300---
15301
15302#### \`--chunkName\`
15303
15304**Type**: String
15305**Description**: Logical name of the asset chunk (e.g. \`app\`, \`vendor\`).
15306
15307**Example**:
15308\`\`\`bash
15309--chunkName="app"
15310\`\`\`
15311
15312---
15313
15314#### \`--chunkVersionId\`
15315
15316**Type**: String
15317**Description**: Version identifier for this chunk (e.g. content hash or build id). Chunks are deduped by (chunkName, chunkVersionId).
15318
15319**Example**:
15320\`\`\`bash
15321--chunkVersionId="$CI_COMMIT_SHA"
15322\`\`\`
15323
15324---
15325
15326#### \`--chunkAssetsDirectory\`
15327
15328**Type**: String (path)
15329**Description**: Directory whose contents should be packaged into this chunk.
15330
15331**Example**:
15332\`\`\`bash
15333--chunkAssetsDirectory="dist"
15334\`\`\`
15335
15336---
15337
15338### Optional Flags
15339
15340#### \`--chunkAssetsDirectoryPrefix\`
15341
15342**Type**: String
15343**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.
15344
15345**Example**:
15346\`\`\`bash
15347--chunkAssetsDirectoryPrefix="static/assets"
15348\`\`\`
15349
15350---
15351
15352#### \`--commitSha\`
15353
15354**Type**: String
15355**Description**: Commit SHA being tested
15356**Default**: Auto-detected from git
15357
15358---
15359
15360#### \`--force\`
15361
15362**Type**: Boolean
15363**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
15363write the existing chunk; downstream test runs that already referenced the old bytes will resolve to the new ones.
15364**Default**: \`false\`
15365
15366**Example**:
15367\`\`\`bash
15368--force
15369\`\`\`
15370
15371---
15372
15373### Complete Example
15374
15375\`\`\`bash
15376npx @alwaysmeticulous/cli ci upload-asset-chunk \\
15377  --apiToken="$METICULOUS_API_TOKEN" \\
15378  --chunkName="app" \\
15379  --chunkVersionId="$CI_COMMIT_SHA" \\
15380  --chunkAssetsDirectory="dist"
15381\`\`\`
15382
15383---
15384
15385### Exit Codes
15386
15387| Code | Meaning |
15388|------|---------|
15389| 0 | Success |
15390| 1 | Failure |
15391| 2 | Error |
15392
15393---
15394
15395## ci run-with-uploaded-asset-chunks
15396
15397Trigger a test run against already-uploaded asset chunks. Pair with \`ci upload-asset-chunk\`.
15398
15399### Synopsis
15400
15401\`\`\`bash
15402npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
15403  --apiToken="<token>" \\
15404  --commitSha="<sha>" \\
15405  --assetReferencesManifest="<path>" \\
15406  [options]
15407\`\`\`
15408
15409### Required Flags
15410
15411#### \`--apiToken\`
15412
15413**Type**: String
15414**Description**: Your Meticulous API token
15415
15416---
15417
15418#### \`--assetReferencesManifest\`
15419
15420**Type**: String (path)
15421**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\`.
15422
15423**File format**:
15424\`\`\`json
15425[
15426  { "name": "app", "versionId": "ad8a8da9aaaweaad9" },
15427  { "name": "plugin-1", "versionLookup": "latest-in-history" }
15428]
15429\`\`\`
15430
15431**Example**:
15432\`\`\`bash
15433--assetReferencesManifest="./manifest.json"
15434\`\`\`
15435
15436---
15437
15438### Optional Flags
15439
15440#### \`--commitSha\`
15441
15442**Type**: String
15443**Description**: Commit SHA being tested
15444**Default**: Auto-detected from git
15445
15446---
15447
15448#### \`--baseSha\`
15449
15450**Type**: String
15451**Description**: The base commit SHA to compare against. Intended for custom test run triggers. Cannot be combined with \`--repoDirectory\`.
15452
15453---
15454
15455#### \`--gitDiffOutput\`
15456
15457**Type**: String
15458**Description**: Raw git diff output between the base and head commits. Requires \`--baseSha\`. Cannot be combined with \`--repoDirectory\`.
15459
15460---
15461
15462#### \`--repoDirectory\`
15463
15464**Type**: String (path)
15465**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\`.
15466
15467---
15468
15469#### \`--rewrites\`
15470
15471**Type**: String (JSON)
15472**Description**: URL rewrite rules in Vercel \`serve-handler\` format.
15473**Default**: \`'[]'\` (falls back to \`{ source: "**", destination: "/index.html" }\`)
15474
15475**Example**:
15476\`\`\`bash
15477--rewrites='[{"source":"/(.*)", "destination":"/index.html"}]'
15478\`\`\`
15479
15480---
15481
15482#### \`--sessionFilter\`
15483
15484**Type**: String (path)
15485**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).
15486
15487**File format**:
15488\`\`\`json
15489{
15490  "session-start-url-matches-any-regex": ["/checkout/", "/settings/"]
15491}
15492\`\`\`
15493
15494**Example**:
15495\`\`\`bash
15496--sessionFilter="./session-filter.json"
15497\`\`\`
15498
15499If the filter excludes every session, no test run is triggered and the command exits with code \`4\` rather than the
15500generic \`1\`, so a pipeline can tell "nothing to test" apart from a real failure.
15501
15502---
15503
15504#### \`--waitForBase\`
15505
15506**Type**: Boolean
15507**Description**: If true, wait for a base test run to be created before triggering a test run.
15508**Default**: \`true\`
15509
15510---
15511
15512#### \`--waitForTestRunToComplete\`
15513
15514**Type**: Boolean
15515**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\`.
15516**Default**: \`false\`
15517
15518---
15519
15520#### \`--dryRun\`
15521
15522**Type**: Boolean
15523**Description**: Print what would be triggered without making the API call.
15524**Default**: \`false\`
15525
15526---
15527
15528#### \`--json\`
15529
15530**Type**: Boolean
15531**Description**: Print one machine-readable JSON result on stdout. See [Machine-readable output](#machine-readable-output).
15532**Default**: \`false\`
15533
15534---
15535
15536### Complete Example
15537
15538\`\`\`bash
15539# manifest.json
15540# [
15541#   { "name": "app", "versionId": "ad8a8da9aaaweaad9" },
15542#   { "name": "plugin-1", "versionId": "dd8ffdaa9dfedebb3" }
15543# ]
15544
15545npx @alwaysmeticulous/cli ci run-with-uploaded-asset-chunks \\
15546  --apiToken="$METICULOUS_API_TOKEN" \\
15547  --commitSha="$CI_COMMIT_SHA" \\
15548  --assetReferencesManifest="./manifest.json"
15549\`\`\`
15550
15551---
15552
15553### Exit Codes
15554
15555| Code | Meaning |
15556|------|---------|
15557| 0 | Success |
15558| 1 | Failure |
15559| 4 | No test run triggered: \`--sessionFilter\` excluded every session that would otherwise have been replayed |
15560
15561---
15562
15563## simulate
15564
15565Replay a session locally for debugging.
15566
15567### Synopsis
15568
15569\`\`\`bash
15570npx @alwaysmeticulous/cli simulate \\
15571  --sessionId="<id>" \\
15572  --appUrl="<url>" \\
15573  [options]
15574\`\`\`
15575
15576### Required Flags
15577
15578#### \`--sessionId\`
15579
15580**Type**: String
15581**Description**: ID of session to replay
15582**How to get**: From Meticulous dashboard or test run
15583
15584**Example**:
15585\`\`\`bash
15586--sessionId="ses_abc123..."
15587\`\`\`
15588
15589---
15590
15591#### \`--appUrl\`
15592
15593**Type**: String
15594**Description**: URL where your app is running locally
15595
15596**Example**:
15597\`\`\`bash
15598--appUrl="http://localhost:3000"
15599\`\`\`
15600
15601---
15602
15603### Optional Flags
15604
15605#### \`--apiToken\`
15606
15607**Type**: String
15608**Description**: Your Meticulous API token
15609**Note**: Required if session is private
15610
15611---
15612
15613#### \`--headless\`
15614
15615**Type**: Boolean
15616**Description**: Run browser in headless mode
15617**Default**: false
15618
15619**Example**:
15620\`\`\`bash
15621--headless
15622\`\`\`
15623
15624---
15625
15626#### \`--devtools\`
15627
15628**Type**: Boolean
15629**Description**: Open browser DevTools automatically
15630**Default**: false
15631
15632**Example**:
15633\`\`\`bash
15634--devtools
15635\`\`\`
15636
15637---
15638
15639### Complete Example
15640
15641\`\`\`bash
15642# Basic replay
15643npx @alwaysmeticulous/cli simulate \\
15644  --sessionId="ses_abc123..." \\
15645  --appUrl="http://localhost:3000"
15646
15647# With DevTools for debugging
15648npx @alwaysmeticulous/cli simulate \\
15649  --sessionId="ses_abc123..." \\
15650  --appUrl="http://localhost:3000" \\
15651  --devtools
15652
15653# Headless mode for CI
15654npx @alwaysmeticulous/cli simulate \\
15655  --sessionId="ses_abc123..." \\
15656  --appUrl="http://localhost:3000" \\
15657  --headless
15658\`\`\`
15659
15660---
15661
15662### Exit Codes
15663
15664| Code | Meaning |
15665|------|---------|
15666| 0 | Replay completed successfully |
15667| 1 | Replay failed |
15668
15669---
15670
15671## crawl
15672
15673Crawl 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.
15674
15675Passing 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.
15676
15677{% callout type="warning" %}
15678Recording starts as soon as the browser opens, so the login flow (including any credentials you type) is recorded as part of the first session.
15679{% /callout %}
15680
15681### Synopsis
15682
15683\`\`\`bash
15684npx @alwaysmeticulous/cli crawl \\
15685  --apiToken="<token>" \\
15686  --startUrl="<url>" \\
15687  [options]
15688\`\`\`
15689
15690### Required Flags
15691
15692#### \`--startUrl\`
15693
15694**Type**: String (repeatable)
15695**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
15696
15697**Example**:
15698\`\`\`bash
15699--startUrl="https://app.example.com"
15700
15701# Several URLs behind one login
15702--startUrl="https://app.example.com/dashboard" \\
15703  --startUrl="https://app.example.com/settings" \\
15704  --startUrl="https://app.example.com/billing"
15705\`\`\`
15706
15707---
15708
15709### Optional Flags
15710
15711#### \`--apiToken\`
15712
15713**Type**: String
15714**Description**: The API token of the project to record sessions into
15715**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
15716
15717---
15718
15719#### \`--crawlingTimeoutSeconds\`
15720
15721**Type**: Number
15722**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)
15723**Default**: 120
15724
15725---
15726
15727#### \`--maxNumSessions\`
15728
15729**Type**: Number
15730**Description**: The maximum number of sessions to record
15731**Default**: 200
15732
15733---
15734
15735#### \`--skipTestRun\`
15736
15737**Type**: Boolean
15738**Description**: Don't create a test run from the recorded sessions
15739**Default**: false
15740
15741---
15742
15743### Complete Example
15744
15745\`\`\`bash
15746# Crawl for 2 minutes and create a test run from the recorded sessions
15747npx @alwaysmeticulous/cli crawl \\
15748  --apiToken="<token>" \\
15749  --startUrl="https://app.example.com"
15750
15751# Longer crawl, sessions only (no test run)
15752npx @alwaysmeticulous/cli crawl \\
15753  --apiToken="<token>" \\
15754  --startUrl="https://app.example.com" \\
15755  --crawlingTimeoutSeconds=600 \\
15756  --skipTestRun
15757
15758# A list of URLs behind one login, 10 minutes shared between them
15759npx @alwaysmeticulous/cli crawl \\
15760  --apiToken="<token>" \\
15761  --startUrl="https://app.example.com/dashboard" \\
15762  --startUrl="https://app.example.com/settings" \\
15763  --startUrl="https://app.example.com/billing" \\
15764  --crawlingTimeoutSeconds=600
15765\`\`\`
15766
15767When 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.
15768
15769---
15770
15771### Exit Codes
15772
15773| Code | Meaning |
15774|------|---------|
15775| 0 | Crawl completed successfully |
15776| 1 | Crawl failed or no sessions were recorded |
15777
15778---
15779
15780## ci start-tunnel
15781
15782Start a secure tunnel for manual testing and debugging.
15783
15784### Synopsis
15785
15786\`\`\`bash
15787npx @alwaysmeticulous/cli ci start-tunnel \\
15788  --port=<port> \\
15789  [options]
15790\`\`\`
15791
15792### Required Flags
15793
15794#### \`--port\` / \`-p\`
15795
15796**Type**: Number
15797**Description**: Port your local server is running on
15798
15799**Example**:
15800\`\`\`bash
15801--port=3000
15802-p 3000
15803\`\`\`
15804
15805---
15806
15807### Optional Flags
15808
15809#### \`--apiToken\`
15810
15811**Type**: String
15812**Description**: Your Meticulous API token
15813**Note**: Required for authentication
15814
15815---
15816
15817#### \`--localHost\` / \`-l\`
15818
15819**Type**: String
15820**Description**: Host to tunnel to
15821**Default**: localhost
15822
15823**Example**:
15824\`\`\`bash
15825--localHost=127.0.0.1
15826\`\`\`
15827
15828---
15829
15830#### \`--localHttps\`
15831
15832**Type**: Boolean
15833**Description**: Connect to local HTTPS server
15834**Default**: false
15835
15836**Example**:
15837\`\`\`bash
15838--localHttps
15839\`\`\`
15840
15841---
15842
15843#### \`--localCert\`
15844
15845**Type**: String (path)
15846**Description**: Path to SSL certificate file
15847
15848**Example**:
15849\`\`\`bash
15850--localCert="./certs/server.crt"
15851\`\`\`
15852
15853---
15854
15855#### \`--localKey\`
15856
15857**Type**: String (path)
15858**Description**: Path to SSL key file
15859
15860**Example**:
15861\`\`\`bash
15862--localKey="./certs/server.key"
15863\`\`\`
15864
15865---
15866
15867#### \`--localCa\`
15868
15869**Type**: String (path)
15870**Description**: Path to CA file for self-signed certificates
15871
15872**Example**:
15873\`\`\`bash
15874--localCa="./certs/ca.crt"
15875\`\`\`
15876
15877---
15878
15879#### \`--allowInvalidCert\`
15880
15881**Type**: Boolean
15882**Description**: Ignore SSL certificate errors
15883**Default**: false
15884
15885**Example**:
15886\`\`\`bash
15887--allowInvalidCert
15888\`\`\`
15889
15890---
15891
15892#### \`--proxyAllUrls\`
15893
15894**Type**: Boolean
15895**Description**: Proxy all URLs through tunnel
15896**Default**: false
15897
15898---
15899
15900#### \`--rewriteHostnameToAppUrl\`
15901
15902**Type**: Boolean
15903**Description**: Rewrite request hostnames
15904**Default**: false
15905
15906---
15907
15908#### \`--enableDnsCache\`
15909
15910**Type**: Boolean
15911**Description**: Enable DNS caching
15912**Default**: false
15913
15914---
15915
15916#### \`--printRequests\`
15917
15918**Type**: Boolean
15919**Description**: Log all requests through tunnel
15920**Default**: false
15921
15922**Example**:
15923\`\`\`bash
15924--printRequests
15925\`\`\`
15926
15927---
15928
15929#### \`--http2Connections\`
15930
15931**Type**: Number
15932**Description**: Number of HTTP/2 connections for multiplexing
15933**Default**: Number of CPU cores
15934
15935**Example**:
15936\`\`\`bash
15937--http2Connections=8
15938\`\`\`
15939
15940---
15941
15942### Complete Example
15943
15944\`\`\`bash
15945# Basic tunnel
15946npx @alwaysmeticulous/cli ci start-tunnel \\
15947  --port=3000
15948
15949# With request logging
15950npx @alwaysmeticulous/cli ci start-tunnel \\
15951  --port=3000 \\
15952  --printRequests
15953
15954# HTTPS tunnel with self-signed cert
15955npx @alwaysmeticulous/cli ci start-tunnel \\
15956  --port=3000 \\
15957  --localHttps \\
15958  --allowInvalidCert
15959
15960# Multi-server setup
15961npx @alwaysmeticulous/cli ci start-tunnel \\
15962  --port=3000 \\
15963  --proxyAllUrls
15964\`\`\`
15965
15966---
15967
15968### Output
15969
15970When tunnel starts successfully:
15971
15972\`\`\`
15973Your url is: https://abc123.meticulous.ai
15974user: meticulous, password: ******
15975\`\`\`
15976
15977Use these credentials to access your app through the tunnel.
15978
15979---
15980
15981## ci prepare
15982
15983Ensure a base test run exists before running tests.
15984
15985### Synopsis
15986
15987\`\`\`bash
15988npx @alwaysmeticulous/cli ci prepare \\
15989  --apiToken="<token>" \\
15990  [options]
15991\`\`\`
15992
15993### Required Flags
15994
15995#### \`--apiToken\`
15996
15997**Type**: String
15998**Description**: Your Meticulous API token
15999
16000---
16001
16002### Required Flags
16003
16004#### \`--triggerScript\`
16005
16006**Type**: String
16007**Description**: Path to script that triggers a test run on a specific commit
16008
16009---
16010
16011### Optional Flags
16012
16013#### \`--headCommit\`
16014
16015**Type**: String
16016**Description**: Commit SHA to check/prepare
16017**Default**: Auto-detected
16018
16019---
16020
16021### Complete Example
16022
16023\`\`\`bash
16024npx @alwaysmeticulous/cli ci prepare \\
16025  --apiToken="$METICULOUS_API_TOKEN" \\
16026  --triggerScript="./scripts/trigger-test-run.sh"
16027\`\`\`
16028
16029---
16030
16031## ci label-commit
16032
16033Attach 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.
16034
16035### Synopsis
16036
16037\`\`\`bash
16038npx @alwaysmeticulous/cli ci label-commit \\
16039  --apiToken="<token>" \\
16040  --labels not-relevant \\
16041  [options]
16042\`\`\`
16043
16044### Required Flags
16045
16046#### \`--apiToken\`
16047
16048**Type**: String
16049**Description**: Your Meticulous API token
16050
16051---
16052
16053#### \`--labels\`
16054
16055**Type**: String (list)
16056**Description**: The labels to attach to the commit. Supported labels: \`not-relevant\`
16057
16058---
16059
16060### Optional Flags
16061
16062#### \`--commitSha\`
16063
16064**Type**: String
16065**Description**: The commit to label
16066**Default**: Auto-detected from git
16067
16068---
16069
16070### Complete Example
16071
16072\`\`\`bash
16073npx @alwaysmeticulous/cli ci label-commit \\
16074  --apiToken="$METICULOUS_API_TOKEN" \\
16075  --labels not-relevant
16076\`\`\`
16077
16078---
16079
16080## Common Patterns
16081
16082### Environment Variables
16083
16084Set API token via environment variable:
16085
16086\`\`\`bash
16087export METICULOUS_API_TOKEN="met_live_abc123..."
16088
16089# Now can omit --apiToken flag
16090npx @alwaysmeticulous/cli ci run-with-tunnel \\
16091  --appUrl="http://localhost:3000"
16092\`\`\`
16093
16094---
16095
16096### CI Integration
16097
16098#### GitHub Actions
16099
16100\`\`\`yaml
16101- name: Run Meticulous tests
16102  run: |
16103    npx @alwaysmeticulous/cli ci run-with-tunnel \\
16104      --apiToken="\${{ secrets.METICULOUS_API_TOKEN }}" \\
16105      --appUrl="http://localhost:3000"
16106\`\`\`
16107
16108#### GitLab CI
16109
16110\`\`\`yaml
16111script:
16112  - >
16113    npx @alwaysmeticulous/cli ci upload-assets
16114    --apiToken="$METICULOUS_API_TOKEN"
16115    --appDirectory="dist"
16116    --commitSha="$CI_COMMIT_SHA"
16117\`\`\`
16118
16119---
16120
16121### Debug Mode
16122
16123Enable verbose logging:
16124
16125\`\`\`bash
16126DEBUG=meticulous:* npx @alwaysmeticulous/cli ci run-with-tunnel \\
16127  --apiToken="$METICULOUS_API_TOKEN" \\
16128  --appUrl="http://localhost:3000"
16129\`\`\`
16130
16131---
16132
16133### Scripting
16134
16135Use in shell scripts:
16136
16137\`\`\`bash
16138#!/bin/bash
16139set -e
16140
16141# Start app
16142npm start &
16143APP_PID=$!
16144
16145# Wait for app
16146npx wait-on http://localhost:3000
16147
16148# Run tests
16149npx @alwaysmeticulous/cli ci run-with-tunnel \\
16150  --apiToken="$METICULOUS_API_TOKEN" \\
16151  --appUrl="http://localhost:3000"
16152
16153# Cleanup
16154kill $APP_PID
16155\`\`\`
16156
16157---
16158
16159### Machine-readable output
16160
16161\`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\`.
16162
16163\`--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.
16164
16165The result is one of three shapes, told apart by \`outcome\`:
16166
16167\`\`\`json
16168{
16169  "cliVersion": "x.y.z",
16170  "outcome": "success",
16171  "testRunId": "…",
16172  "status": null,
16173  "sourceDeploymentId": "…",
16174  "testRunUrl": "https://app.meticulous.ai/projects/<org>/<project>/test-runs/<testRunId>"
16175}
16176\`\`\`
16177
16178\`\`\`json
16179{
16180  "cliVersion": "x.y.z",
16181  "outcome": "skipped",
16182  "reason": "comments_disabled_for_author",
16183  "message": "Test run skipped because CI comments and checks are disabled for this pull request author.",
16184  "testRunId": null,
16185  "status": null,
16186  "sourceDeploymentId": "…"
16187}
16188\`\`\`
16189
16190\`\`\`json
16191{
16192  "cliVersion": "x.y.z",
16193  "outcome": "failed",
16194  "reason": "remote",
16195  "message": "…"
16196}
16197\`\`\`
16198
16199| Field | Present | Meaning |
16200|-------|---------|---------|
16201| \`cliVersion\` | Always | Version of \`@alwaysmeticulous/cli\` that produced the result |
16202| \`outcome\` | Always | \`success\`, \`skipped\`, or \`failed\` |
16203| \`reason\` | \`skipped\` and \`failed\` | Why the run was skipped or failed (see below) |
16204| \`message\` | \`skipped\` and \`failed\` | Human-readable explanation |
16205| \`testRunId\` | \`success\` and \`skipped\`, and \`failed\` once a test run exists | ID of the test run, or \`null\` when none was created |
16206| \`status\` | \`success\` and \`skipped\` | On \`success\` with \`--waitForTestRunToComplete\`, the final test-run status. Otherwise \`null\` |
16207| \`sourceDeploymentId\` | When a deployment was created | ID of the uploaded build. This is not the test run ID |
16208| \`testRunUrl\` | When a test run was created | Link to the test run |
16209
16210\`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\`.
16211
16212Skip 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\`.
16213
16214Failure 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
16214iculous API returned an error), and \`unexpected\`.
16215
16216A \`comments_disabled_for_author\` skip exits with code 0.
16217
16218Upload sizes and part-by-part progress are not included in the JSON. Pass \`--logLevel info\` to get them on stderr.
16219
16220---
16221
16222## Troubleshooting
16223
16224### "API token required"
16225
16226**Cause**: No API token provided
16227
16228**Solution**: Pass \`--apiToken\` or set \`METICULOUS_API_TOKEN\` env var
16229
16230---
16231
16232### "Failed to connect"
16233
16234**Cause**: App not running or wrong URL
16235
16236**Solutions**:
162371. Verify app is running: \`curl http://localhost:3000\`
162382. Check port in \`--appUrl\` matches actual port
162393. Increase wait time before running command
16240
16241---
16242
16243### "No sessions found"
16244
16245**Cause**: No recorded sessions for project
16246
16247**Solution**: Record sessions first (add recorder snippet to app)
16248
16249---
16250
16251### "Tunnel connection failed"
16252
16253**Cause**: Network/firewall issue
16254
16255**Solutions**:
162561. Check outbound HTTPS (443) is allowed
162572. Try \`--printRequests\` to debug
162583. Contact support if persists
16259
16260---
16261
16262## See Also
16263
16264- [GitHub Actions Setup](${o.GITHUB_ACTIONS_SETUP_URL}) - GitHub Actions configuration
16265- [Tunnel Advanced Options](${o.TUNNEL_ADVANCED_OPTIONS_URL}) - Detailed tunnel configuration
16266- [FAQ & Troubleshooting](${o.FAQ_AND_TROUBLESHOOTING_URL}) - Common issues and solutions
16267`},{id:"environment-variables",url:"/docs/reference/environment-variables",document:`---
16268{
16269  "title": "Environment Variables Reference"
16270}
16271---
16272
16273# {% $frontmatter.title %}
16274
16275Reference 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.
16276
16277Set these in your shell before running [CLI commands](${o.CLI_COMMANDS_URL}):
16278
16279\`\`\`bash
16280METICULOUS_HOLD_BROWSER_OPEN=true npx @alwaysmeticulous/cli simulate \\
16281  --sessionId="<id>" --appUrl="<url>"
16282\`\`\`
16283
16284---
16285
16286## Debugging & Inspection
16287
16288### \`METICULOUS_HOLD_BROWSER_OPEN\`
16289
16290**Type**: Boolean (\`true\`/\`false\`)
16291
16292Keep the browser open after a replay completes so you can inspect the final state, open DevTools, and explore the DOM.
16293
16294\`\`\`bash
16295METICULOUS_HOLD_BROWSER_OPEN=true npx @alwaysmeticulous/cli simulate \\
16296  --sessionId="<id>" --appUrl="<url>"
16297\`\`\`
16298
16299---
16300
16301### \`METICULOUS_SHOW_MOUSE_LOCATION\`
16302
16303**Type**: Boolean (\`true\`/\`false\`)
16304
16305Displays the mouse position as a red dot on the page during replay. Helpful for verifying that mouse events are targeting the correct elements.
16306
16307---
16308
16309### \`METICULOUS_TRACK_UNEXPECTED_EXECUTION\`
16310
16311**Type**: Boolean (\`true\`/\`false\`)
16312
16313Enables 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.
16314
16315**Tips**:
16316- Run \`new Error().stack\` in the console when paused to get a stack trace
16317- In the Chromium debugger, tick "Show ignore-listed frames" when viewing stack traces
16318
16319> **Note**: This must be enabled for \`METICULOUS_UNEXPECTED_EXECUTION_AUTO_RESUME\` and \`METICULOUS_SET_BREAKPOINTS\` to take effect.
16320
16321---
16322
16323### \`METICULOUS_UNEXPECTED_EXECUTION_AUTO_RESUME\`
16324
16325**Type**: Boolean (\`true\`/\`false\`)
16326
16327Automatically resumes when unexpected execution is encountered instead of pausing. Use this when you want to see the logs without manually stepping through each pause.
16328
16329Requires \`METICULOUS_TRACK_UNEXPECTED_EXECUTION=true\`.
16330
16331---
16332
16333### \`METICULOUS_SET_BREAKPOINTS\`
16334
16335**Type**: JSON string
16336
16337Set breakpoints as a JSON array. You can copy breakpoint strings from the logs when \`METICULOUS_TRACK_UNEXPECTED_EXECUTION\` is enabled.
16338
16339Breakpoints can be specified as objects:
16340
16341\`\`\`bash
16342METICULOUS_SET_BREAKPOINTS='[{"scriptId": "<id>", "lineNumber": 10, "columnNumber": 5}]'
16343\`\`\`
16344
16345Or as URL strings:
16346
16347\`\`\`bash
16348METICULOUS_SET_BREAKPOINTS='["https://example.com/script.js:10:5"]'
16349\`\`\`
16350
16351---
16352
16353### \`METICULOUS_DEBUG_DOM_UPDATES\`
16354
16355**Type**: Boolean (\`true\`/\`false\`)
16356
16357Logs details of DOM mutations that occur at unexpected times (e.g. outside of \`advanceVirtualTime\`), including the HTML of the mutated elements.
16358
16359---
16360
16361### \`METICULOUS_PAUSE_BEFORE_REDIRECT\`
16362
16363**Type**: String (URL fragment)
16364
16365Pauses the browser before any redirects whose URL contains the specified fragment. For example, to pause before redirecting to a login page:
16366
16367\`\`\`bash
16368METICULOUS_PAUSE_BEFORE_REDIRECT=login
16369\`\`\`
16370
16371---
16372
16373## Timing & Timeouts
16374
16375### \`METICULOUS_NO_TIMEOUT\`
16376
16377**Type**: Boolean (\`true\`/\`false\`)
16378
16379Disables all timeouts except for the navigation timeout (which is extended to 60 minutes). Useful when pausing in debuggers or stepping through replay execution.
16380
16381---
16382
16383### \`METICULOUS_REPLAY_TIMEOUT_MINUTES\`
16384
16385**Type**: Number (minutes)
16386
16387Sets the replay timeout to the specified number of minutes, overriding the default.
16388
16389\`\`\`bash
16390METICULOUS_REPLAY_TIMEOUT_MINUTES=10
16391\`\`\`
16392
16393---
16394
16395### \`METICULOUS_MAX_DURATION_MS\`
16396
16397**Type**: Number (milliseconds)
16398
16399Cuts 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.
16400
16401\`\`\`bash
16402METICULOUS_MAX_DURATION_MS=30000
16403\`\`\`
16404
16405---
16406
16407## Browser Configuration
16408
16409### \`METICULOUS_ADDITIONAL_CHROMIUM_FLAGS\`
16410
16411**Type**: String (comma-separated flags)
16412
16413Passes additional flags to the Chromium browser instance launched for replay.
16414
16415\`\`\`bash
16416METICULOUS_ADDITIONAL_CHROMIUM_FLAGS="--disable-gpu,--no-sandbox"
16417\`\`\`
16418
16419
16420---
16421
16422## Feature Toggles
16423
16424### \`METICULOUS_DISABLE_SENTRY\`
16425
16426**Type**: Boolean (\`true\`/\`false\`)
16427
16428Disables the customer application's Sentry initialization during replay. This simplifies stack traces and reduces noise when debugging replay issues.
16429
16430---
16431
16432### \`METICULOUS_DISABLE_RECAPTCHA\`
16433
16434**Type**: Boolean (\`true\`/\`false\`)
16435
16436Disables Google reCAPTCHA script loading during replay. This simplifies stack traces and reduces noise when debugging.
16437
16438---
16439
16440### \`METICULOUS_DISABLE_WEB_WORKERS\`
16441
16442**Type**: Boolean (\`true\`/\`false\`)
16443
16444Disables 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.
16445
16446---
16447
16448### \`METICULOUS_DISABLE_SHARED_WORKERS\`
16449
16450**Type**: Boolean (\`true\`/\`false\`)
16451
16452Disables Shared Workers (\`window.SharedWorker\`) during replay.
16453
16454
16455`},{id:"performance-api",url:"/docs/reference/performance-api",document:`---
16456{
16457  "title": "Performance API Reference"
16458}
16459---
16460
16461# {% $frontmatter.title %}
16462
16463{% callout_card variant="info" title="Actively under development" %}
16464The 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]).
16465{% /callout_card %}
16466
16467Meticulous 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.
16468
16469When 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.
16470
16471---
16472
16473## Overview
16474
16475During a Meticulous replay:
16476
16477- \`performance.now()\`, \`Date.now()\`, and other timing APIs return **virtual** (deterministic) values.
16478- \`performance.memory\` returns **fixed** values.
16479- \`performance.measureUserAgentSpecificMemory()\` returns **fixed** values.
16480- \`PerformanceObserver\` is stubbed.
16481- \`PressureObserver\` is stubbed.
16482- \`setTimeout\`, \`setInterval\`, \`clearTimeout\`, and \`clearInterval\` schedule callbacks on **virtual** (deterministic) time.
16483
16484The 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.
16485
16486{% callout_card variant="warning" title="Frontend performance only" %}
16487This API measures **frontend performance only**: rendering time, JavaScript execution, and memory usage.
16488
16489During 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.
16490{% /callout_card %}
16491
16492---
16493
16494## When to Use
16495
16496Only collect and report metrics when \`isBenchmarkableReplay\` is \`true\`. This flag indicates the replay was executed under conditions where performance data is meaningful.
16497
16498\`\`\`typescript
16499if (window.Meticulous?.replay?.isBenchmarkableReplay) {
16500  // Safe to collect and report performance metrics
16501}
16502\`\`\`
16503
16504---
16505
16506## Available APIs
16507
16508All APIs live on \`window.Meticulous.replay.native\`.
16509
16510### \`native.performance.now()\`
16511
16512Returns 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).
16513
16514**Returns**: \`number\`
16515
16516\`\`\`typescript
16517const realElapsed = window.Meticulous.replay.native.performance.now();
16518\`\`\`
16519
16520Use this wherever you would normally use \`performance.now()\` to measure durations:
16521
16522\`\`\`typescript
16523const perf = window.Meticulous?.replay?.isBenchmarkableReplay
16524  ? window.Meticulous.replay.native.performance
16525  : window.performance;
16526
16527const start = perf.now();
16528doExpensiveWork();
16529const duration = perf.now() - start;
16530\`\`\`
16531
16532---
16533
16534### \`native.performance.memory\`
16535
16536**Type**: \`{ jsHeapSizeLimit: number; totalJSHeapSize: number; usedJSHeapSize: number } | undefined\`
16537
16538Returns 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).
16539
16540| Property | Type | Description |
16541|----------|------|-------------|
16542| \`jsHeapSizeLimit\` | \`number\` | Maximum heap size in bytes |
16543| \`totalJSHeapSize\` | \`number\` | Total allocated heap in bytes |
16544| \`usedJSHeapSize\` | \`number\` | Currently used heap in bytes |
16545
16546\`\`\`typescript
16547const mem = window.Meticulous.replay.native.performance.memory;
16548if (mem) {
16549  console.log("Heap used:", mem.usedJSHeapSize);
16550}
16551\`\`\`
16552
16553---
16554
16555### \`native.performance.measureUserAgentSpecificMemory()\`
16556
16557**Type**: \`(() => Promise<MemoryMeasurement>) | undefined\`
16558
16559Returns 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).
16560
16561The resolved value has the following shape:
16562
16563| Property | Type | Description |
16564|----------|------|-------------|
16565| \`bytes\` | \`number\` | Total memory used, in bytes |
16566| \`breakdown\` | \`Array<{ bytes: number; attribution: unknown[]; types: string[] }>\` | Per-realm/per-type breakdown of \`bytes\` |
16567
16568\`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.
16569
16570\`\`\`typescript
16571const measure = window.Meticulous.replay.native.performance
16572  .measureUserAgentSpecificMemory;
16573
16574if (measure) {
16575  const measurement = await measure();
16576  console.log("Total bytes:", measurement.bytes);
16577  for (const entry of measurement.breakdown) {
16578    console.log(entry.types, entry.bytes);
16579  }
16580}
16581\`\`\`
16582
16583{% callout_card variant="warning" title="Cross-origin isolation and availability" %}
16584The 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.
16585
16586However, 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.
16587{% /callout_card %}
16588
16589---
16590
16591### \`native.PerformanceObserver\`
16592
16593**Type**: \`typeof PerformanceObserver\`
16594
16595The 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.
16596
16597\`\`\`typescript
16598const observer = new window.Meticulous.replay.native.PerformanceObserver(
16599  (list) => {
16600    for (const entry of list.getEntries()) {
16601      reportMetric(entry.name, entry.duration);
16602    }
16603  }
16604);
16605observer.observe({ entryTypes: ["measure", "navigation"] });
16606\`\`\`
16607
16608---
16609
16610### \`native.PressureObserver\`
16611
16612**Type**: \`typeof PressureObserver | undefined\`
16613
16614The 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.
16615
16616Use 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.
16617
16618\`\`\`typescript
16619const replay = window.Meticulous?.replay;
16620const PressureObserver = replay?.native.PressureObserver;
16621
16622if (replay?.isBenchmarkableReplay && PressureObserver) {
16623  const observer = new PressureObserver((records) => {
16624    for (const record of records) {
16625      reportPressure({
16626        source: record.source,
16627        state: record.state,
16628        time: record.time,
16629      });
16630    }
16631  });
16632
16633  observer.observe("cpu", { sampleInterval: 1000 }).catch((err) => {
16634    console.warn("Failed to observe CPU pressure", err);
16635  });
16636}
16637\`\`\`
16638
16639The 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\`.
16640
16641---
16642
16643### \`native.setTimeout()\`, \`native.setInterval()\`, \`native.clearTimeout()\`, and \`native.clearInterval()\`
16644
16645**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.
16646
16647During 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.
16648
16649Use these when you need real-time scheduling — for example, to defer performance metric collection until after the main thread has settled:
16650
16651\`\`\`typescript
16652const replay = window.Meticulous?.replay;
16653if (replay?.isBenchmarkableReplay) {
16654  replay.native.setTimeout(() => {
16655    reportPerformanceMetrics();
16656  }, 0);
16657}
16658\`\`\`
16659
16660{% callout_card variant="warning" title="Use with extreme care" %}
16661These functions run against **real wall-clock time** and their callbacks are **not** synchronized with Meticulous's virtual event loop or screenshot timing.
16662
16663**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.
16664
16665Restrict 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.
16666{% /callout_card %}
16667
16668---
16669
16670## Metadata
16671
16672When reporting metrics you'll typically want to attach context about what is being tested. Two properties on \`window.Meticulous.replay\` provide this:
16673
16674### \`commitUnderTest\`
16675
16676**Type**: \`{ sha: string; baseCommitSha: string | null; branchName: string | null; date: string | null } | undefined\`
16677
16678| Property | Type | Description |
16679|----------|------|-------------|
16680| \`sha\` | \`string\` | Full commit SHA being tested |
16681| \`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 |
16682| \`branchName\` | \`string \\| null\` | Git branch name |
16683| \`date\` | \`string \\| null\` | Commit date in ISO 8601 format |
16684
16685### \`sessionBeingReplayed\`
16686
16687**Type**: \`{ id: string }\`
16688
16689The ID of the recorded session being replayed.
16690
16691### \`browser\`
16692
16693**Type**: \`{ version: string }\`
16694
16695Information about the Chrome/Chromium build that is driving the replay.
16696Useful for tagging reported metrics so that performance dashboards can be
16697sliced by browser version — rendering speed, JavaScript execution time, and
16698memory usage can shift meaningfully between Chrome releases, and aggregating
16699across versions can mask regressions.
16700
16701| Property | Type | Description |
16702|----------|------|-------------|
16703| \`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. |
16704
16705\`\`\`typescript
16706const replay = window.Meticulous?.replay;
16707if (replay) {
16708  console.log("Replay running on Chrome", replay.browser.version);
16709}
16710\`\`\`
16711
16712---
16713
16714## Sending Data to Analytics
16715
16716During replays, Meticulous intercepts and mocks network requests. To let your analytics requests pass through, add the \`meticulous-passthrough\` header set to \`"true"\`:
16717
16718\`\`\`typescript
16719fetch("https://analytics.example.com/metrics", {
16720  method: "POST",
16721  headers: {
16722    "Content-Type": "application/json",
16723    "meticulous-passthrough": "true",
16724  },
16725  body: JSON.stringify(payload),
16726});
16727\`\`\`
16728
16729Without this header, the request will be intercepted by the network stubbing layer and will not reach your analytics endpoint.
16730
16731---
16732
16733## Full Example
16734
16735\`\`\`typescript
16736const reportPerformanceMetrics = () => {
16737  const replay = window.Meticulous?.replay;
16738  if (!replay?.isBenchmarkableReplay) {
16739    return;
16740  }
16741
16742  const elapsed = replay.native.performance.now();
16743  const memory = replay.native.performance.memory;
16744  const commit = replay.commitUnderTest;
16745
16746  fetch("https://analytics.example.com/metrics", {
16747    method: "POST",
16748    headers: {
16749      "Content-Type": "application/json",
16750      "meticulous-passthrough": "true",
16751    },
16752    body: JSON.stringify({
16753      elapsedMs: elapsed,
16754      heapUsedBytes: memory?.usedJSHeapSize,
16755      heapTotalBytes: memory?.totalJSHeapSize,
16756      commitSha: commit?.sha,
16757      baseCommitSha: commit?.baseCommitSha,
16758      branch: commit?.branchName,
16759      sessionId: replay.sessionBeingReplayed.id,
16760      chromeVersion: replay.browser.version,
16761    }),
16762  });
16763};
16764\`\`\`
16765
16766---
16767
16768## Data Noise
16769
16770Treat performance data collected from Meticulous replays as **noisy**. Two factors introduce variance:
16771
16772- **Hardware differences**: Replays may execute on different machines with different CPU, memory, and disk characteristics. Absolute numbers will vary across runs.
16773- **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.
16774
16775Because of this, focus on **trends over time** rather than individual data points. Aggregate across multiple replays and commits to identify meaningful regressions or improvements.
16776
16777---
16778
16779## Requirements
16780
16781- **Gate on \`isBenchmarkableReplay\`**: Always check this before collecting metrics. When \`false\`, the replay conditions do not guarantee meaningful numbers and results should be discarded.
16782- **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.
16783- **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.
16784- **Use the passthrough header**: Without \`meticulous-passthrough: "true"\`, analytics requests will be intercepted and mocked, and your data will never reach your analytics endpoint.
16785- **Frontend metrics only**: Rendering, JavaScript execution, and memory are meaningful. Network and backend latency are not, because all requests return mocked responses.
16786
16787---
16788
16789## See Also
16790
16791- [window.Meticulous API](${o.METICULOUS_WINDOW_OBJECT_URL}) — detecting test mode, pause/resume, custom data
16792- [TypeScript Types](${o.TYPESCRIPT_TYPES_URL}) — importable type definitions for \`window.Meticulous\`
16793`}];e.s(["DOC_REGISTRY",0,th,"getDocumentByDocsUrl",0,e=>th.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
16793]!==_?(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)}]);
16794
16795//# debugId=38892b9f-391b-43fc-ef91-6db054dcdb57

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.