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'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 392 393Select the actions tab within the secrets tab: 394 395 396 397And click the new repository secret button: 398 399 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 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'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 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're using a monorepo, you'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'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'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 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 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 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 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 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 13268 13269The second is your GitHub Actions workflow that triggers Meticulous: 13270 13271 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 13277 13278Click the 'Re-run this job' button in the top right: 13279 13280 13281 13282This will open a modal where you can re-run the workflow: 13283 13284 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.