123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555 |
- var {
- _nullishCoalesce,
- _optionalChain
- } = require('@sentry/utils');
- Object.defineProperty(exports, '__esModule', { value: true });
- const core = require('@sentry/core');
- const utils = require('@sentry/utils');
- const tracing = require('@sentry-internal/tracing');
- const WINDOW = utils.GLOBAL_OBJ ;
- const REPLAY_SESSION_KEY = 'sentryReplaySession';
- const REPLAY_EVENT_NAME = 'replay_event';
- const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
- const SESSION_IDLE_PAUSE_DURATION = 300000;
- const SESSION_IDLE_EXPIRE_DURATION = 900000;
- const DEFAULT_FLUSH_MIN_DELAY = 5000;
- const DEFAULT_FLUSH_MAX_DELAY = 5500;
- const BUFFER_CHECKOUT_TIME = 60000;
- const RETRY_BASE_INTERVAL = 5000;
- const RETRY_MAX_COUNT = 3;
- const NETWORK_BODY_MAX_SIZE = 150000;
- const CONSOLE_ARG_MAX_SIZE = 5000;
- const SLOW_CLICK_THRESHOLD = 3000;
- const SLOW_CLICK_SCROLL_TIMEOUT = 300;
- const REPLAY_MAX_EVENT_BUFFER_SIZE = 20000000;
- const MIN_REPLAY_DURATION = 4999;
- const MIN_REPLAY_DURATION_LIMIT = 15000;
- const MAX_REPLAY_DURATION = 3600000;
- function _nullishCoalesce$1(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }function _optionalChain$5(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var NodeType$1;
- (function (NodeType) {
- NodeType[NodeType["Document"] = 0] = "Document";
- NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
- NodeType[NodeType["Element"] = 2] = "Element";
- NodeType[NodeType["Text"] = 3] = "Text";
- NodeType[NodeType["CDATA"] = 4] = "CDATA";
- NodeType[NodeType["Comment"] = 5] = "Comment";
- })(NodeType$1 || (NodeType$1 = {}));
- function isElement$1(n) {
- return n.nodeType === n.ELEMENT_NODE;
- }
- function isShadowRoot(n) {
- const host = _optionalChain$5([n, 'optionalAccess', _ => _.host]);
- return Boolean(_optionalChain$5([host, 'optionalAccess', _2 => _2.shadowRoot]) === n);
- }
- function isNativeShadowDom(shadowRoot) {
- return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]';
- }
- function fixBrowserCompatibilityIssuesInCSS(cssText) {
- if (cssText.includes(' background-clip: text;') &&
- !cssText.includes(' -webkit-background-clip: text;')) {
- cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;');
- }
- return cssText;
- }
- function escapeImportStatement(rule) {
- const { cssText } = rule;
- if (cssText.split('"').length < 3)
- return cssText;
- const statement = ['@import', `url(${JSON.stringify(rule.href)})`];
- if (rule.layerName === '') {
- statement.push(`layer`);
- }
- else if (rule.layerName) {
- statement.push(`layer(${rule.layerName})`);
- }
- if (rule.supportsText) {
- statement.push(`supports(${rule.supportsText})`);
- }
- if (rule.media.length) {
- statement.push(rule.media.mediaText);
- }
- return statement.join(' ') + ';';
- }
- function stringifyStylesheet(s) {
- try {
- const rules = s.rules || s.cssRules;
- return rules
- ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join(''))
- : null;
- }
- catch (error) {
- return null;
- }
- }
- function stringifyRule(rule) {
- let importStringified;
- if (isCSSImportRule(rule)) {
- try {
- importStringified =
- stringifyStylesheet(rule.styleSheet) ||
- escapeImportStatement(rule);
- }
- catch (error) {
- }
- }
- else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
- return fixSafariColons(rule.cssText);
- }
- return importStringified || rule.cssText;
- }
- function fixSafariColons(cssStringified) {
- const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;
- return cssStringified.replace(regex, '$1\\$2');
- }
- function isCSSImportRule(rule) {
- return 'styleSheet' in rule;
- }
- function isCSSStyleRule(rule) {
- return 'selectorText' in rule;
- }
- class Mirror {
- constructor() {
- this.idNodeMap = new Map();
- this.nodeMetaMap = new WeakMap();
- }
- getId(n) {
- if (!n)
- return -1;
- const id = _optionalChain$5([this, 'access', _3 => _3.getMeta, 'call', _4 => _4(n), 'optionalAccess', _5 => _5.id]);
- return _nullishCoalesce$1(id, () => ( -1));
- }
- getNode(id) {
- return this.idNodeMap.get(id) || null;
- }
- getIds() {
- return Array.from(this.idNodeMap.keys());
- }
- getMeta(n) {
- return this.nodeMetaMap.get(n) || null;
- }
- removeNodeFromMap(n) {
- const id = this.getId(n);
- this.idNodeMap.delete(id);
- if (n.childNodes) {
- n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode));
- }
- }
- has(id) {
- return this.idNodeMap.has(id);
- }
- hasNode(node) {
- return this.nodeMetaMap.has(node);
- }
- add(n, meta) {
- const id = meta.id;
- this.idNodeMap.set(id, n);
- this.nodeMetaMap.set(n, meta);
- }
- replace(id, n) {
- const oldNode = this.getNode(id);
- if (oldNode) {
- const meta = this.nodeMetaMap.get(oldNode);
- if (meta)
- this.nodeMetaMap.set(n, meta);
- }
- this.idNodeMap.set(id, n);
- }
- reset() {
- this.idNodeMap = new Map();
- this.nodeMetaMap = new WeakMap();
- }
- }
- function createMirror() {
- return new Mirror();
- }
- function shouldMaskInput({ maskInputOptions, tagName, type, }) {
- if (tagName === 'OPTION') {
- tagName = 'SELECT';
- }
- return Boolean(maskInputOptions[tagName.toLowerCase()] ||
- (type && maskInputOptions[type]) ||
- type === 'password' ||
- (tagName === 'INPUT' && !type && maskInputOptions['text']));
- }
- function maskInputValue({ isMasked, element, value, maskInputFn, }) {
- let text = value || '';
- if (!isMasked) {
- return text;
- }
- if (maskInputFn) {
- text = maskInputFn(text, element);
- }
- return '*'.repeat(text.length);
- }
- function toLowerCase(str) {
- return str.toLowerCase();
- }
- function toUpperCase(str) {
- return str.toUpperCase();
- }
- const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__';
- function is2DCanvasBlank(canvas) {
- const ctx = canvas.getContext('2d');
- if (!ctx)
- return true;
- const chunkSize = 50;
- for (let x = 0; x < canvas.width; x += chunkSize) {
- for (let y = 0; y < canvas.height; y += chunkSize) {
- const getImageData = ctx.getImageData;
- const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData
- ? getImageData[ORIGINAL_ATTRIBUTE_NAME]
- : getImageData;
- const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer);
- if (pixelBuffer.some((pixel) => pixel !== 0))
- return false;
- }
- }
- return true;
- }
- function getInputType(element) {
- const type = element.type;
- return element.hasAttribute('data-rr-is-password')
- ? 'password'
- : type
- ?
- toLowerCase(type)
- : null;
- }
- function getInputValue(el, tagName, type) {
- if (tagName === 'INPUT' && (type === 'radio' || type === 'checkbox')) {
- return el.getAttribute('value') || '';
- }
- return el.value;
- }
- let _id = 1;
- const tagNameRegex = new RegExp('[^a-z0-9-_:]');
- const IGNORED_NODE = -2;
- function genId() {
- return _id++;
- }
- function getValidTagName(element) {
- if (element instanceof HTMLFormElement) {
- return 'form';
- }
- const processedTagName = toLowerCase(element.tagName);
- if (tagNameRegex.test(processedTagName)) {
- return 'div';
- }
- return processedTagName;
- }
- function extractOrigin(url) {
- let origin = '';
- if (url.indexOf('//') > -1) {
- origin = url.split('/').slice(0, 3).join('/');
- }
- else {
- origin = url.split('/')[0];
- }
- origin = origin.split('?')[0];
- return origin;
- }
- let canvasService;
- let canvasCtx;
- const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
- const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
- const URL_WWW_MATCH = /^www\..*/i;
- const DATA_URI = /^(data:)([^,]*),(.*)/i;
- function absoluteToStylesheet(cssText, href) {
- return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => {
- const filePath = path1 || path2 || path3;
- const maybeQuote = quote1 || quote2 || '';
- if (!filePath) {
- return origin;
- }
- if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {
- return `url(${maybeQuote}${filePath}${maybeQuote})`;
- }
- if (DATA_URI.test(filePath)) {
- return `url(${maybeQuote}${filePath}${maybeQuote})`;
- }
- if (filePath[0] === '/') {
- return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`;
- }
- const stack = href.split('/');
- const parts = filePath.split('/');
- stack.pop();
- for (const part of parts) {
- if (part === '.') {
- continue;
- }
- else if (part === '..') {
- stack.pop();
- }
- else {
- stack.push(part);
- }
- }
- return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;
- });
- }
- const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/;
- const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/;
- function getAbsoluteSrcsetString(doc, attributeValue) {
- if (attributeValue.trim() === '') {
- return attributeValue;
- }
- let pos = 0;
- function collectCharacters(regEx) {
- let chars;
- const match = regEx.exec(attributeValue.substring(pos));
- if (match) {
- chars = match[0];
- pos += chars.length;
- return chars;
- }
- return '';
- }
- const output = [];
- while (true) {
- collectCharacters(SRCSET_COMMAS_OR_SPACES);
- if (pos >= attributeValue.length) {
- break;
- }
- let url = collectCharacters(SRCSET_NOT_SPACES);
- if (url.slice(-1) === ',') {
- url = absoluteToDoc(doc, url.substring(0, url.length - 1));
- output.push(url);
- }
- else {
- let descriptorsStr = '';
- url = absoluteToDoc(doc, url);
- let inParens = false;
- while (true) {
- const c = attributeValue.charAt(pos);
- if (c === '') {
- output.push((url + descriptorsStr).trim());
- break;
- }
- else if (!inParens) {
- if (c === ',') {
- pos += 1;
- output.push((url + descriptorsStr).trim());
- break;
- }
- else if (c === '(') {
- inParens = true;
- }
- }
- else {
- if (c === ')') {
- inParens = false;
- }
- }
- descriptorsStr += c;
- pos += 1;
- }
- }
- }
- return output.join(', ');
- }
- function absoluteToDoc(doc, attributeValue) {
- if (!attributeValue || attributeValue.trim() === '') {
- return attributeValue;
- }
- const a = doc.createElement('a');
- a.href = attributeValue;
- return a.href;
- }
- function isSVGElement(el) {
- return Boolean(el.tagName === 'svg' || el.ownerSVGElement);
- }
- function getHref() {
- const a = document.createElement('a');
- a.href = '';
- return a.href;
- }
- function transformAttribute(doc, tagName, name, value, element, maskAttributeFn) {
- if (!value) {
- return value;
- }
- if (name === 'src' ||
- (name === 'href' && !(tagName === 'use' && value[0] === '#'))) {
- return absoluteToDoc(doc, value);
- }
- else if (name === 'xlink:href' && value[0] !== '#') {
- return absoluteToDoc(doc, value);
- }
- else if (name === 'background' &&
- (tagName === 'table' || tagName === 'td' || tagName === 'th')) {
- return absoluteToDoc(doc, value);
- }
- else if (name === 'srcset') {
- return getAbsoluteSrcsetString(doc, value);
- }
- else if (name === 'style') {
- return absoluteToStylesheet(value, getHref());
- }
- else if (tagName === 'object' && name === 'data') {
- return absoluteToDoc(doc, value);
- }
- if (typeof maskAttributeFn === 'function') {
- return maskAttributeFn(name, value, element);
- }
- return value;
- }
- function ignoreAttribute(tagName, name, _value) {
- return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';
- }
- function _isBlockedElement(element, blockClass, blockSelector, unblockSelector) {
- try {
- if (unblockSelector && element.matches(unblockSelector)) {
- return false;
- }
- if (typeof blockClass === 'string') {
- if (element.classList.contains(blockClass)) {
- return true;
- }
- }
- else {
- for (let eIndex = element.classList.length; eIndex--;) {
- const className = element.classList[eIndex];
- if (blockClass.test(className)) {
- return true;
- }
- }
- }
- if (blockSelector) {
- return element.matches(blockSelector);
- }
- }
- catch (e) {
- }
- return false;
- }
- function elementClassMatchesRegex(el, regex) {
- for (let eIndex = el.classList.length; eIndex--;) {
- const className = el.classList[eIndex];
- if (regex.test(className)) {
- return true;
- }
- }
- return false;
- }
- function distanceToMatch(node, matchPredicate, limit = Infinity, distance = 0) {
- if (!node)
- return -1;
- if (node.nodeType !== node.ELEMENT_NODE)
- return -1;
- if (distance > limit)
- return -1;
- if (matchPredicate(node))
- return distance;
- return distanceToMatch(node.parentNode, matchPredicate, limit, distance + 1);
- }
- function createMatchPredicate(className, selector) {
- return (node) => {
- const el = node;
- if (el === null)
- return false;
- try {
- if (className) {
- if (typeof className === 'string') {
- if (el.matches(`.${className}`))
- return true;
- }
- else if (elementClassMatchesRegex(el, className)) {
- return true;
- }
- }
- if (selector && el.matches(selector))
- return true;
- return false;
- }
- catch (e2) {
- return false;
- }
- };
- }
- function needMaskingText(node, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText) {
- try {
- const el = node.nodeType === node.ELEMENT_NODE
- ? node
- : node.parentElement;
- if (el === null)
- return false;
- if (el.tagName === 'INPUT') {
- const autocomplete = el.getAttribute('autocomplete');
- const disallowedAutocompleteValues = [
- 'current-password',
- 'new-password',
- 'cc-number',
- 'cc-exp',
- 'cc-exp-month',
- 'cc-exp-year',
- 'cc-csc',
- ];
- if (disallowedAutocompleteValues.includes(autocomplete)) {
- return true;
- }
- }
- let maskDistance = -1;
- let unmaskDistance = -1;
- if (maskAllText) {
- unmaskDistance = distanceToMatch(el, createMatchPredicate(unmaskTextClass, unmaskTextSelector));
- if (unmaskDistance < 0) {
- return true;
- }
- maskDistance = distanceToMatch(el, createMatchPredicate(maskTextClass, maskTextSelector), unmaskDistance >= 0 ? unmaskDistance : Infinity);
- }
- else {
- maskDistance = distanceToMatch(el, createMatchPredicate(maskTextClass, maskTextSelector));
- if (maskDistance < 0) {
- return false;
- }
- unmaskDistance = distanceToMatch(el, createMatchPredicate(unmaskTextClass, unmaskTextSelector), maskDistance >= 0 ? maskDistance : Infinity);
- }
- return maskDistance >= 0
- ? unmaskDistance >= 0
- ? maskDistance <= unmaskDistance
- : true
- : unmaskDistance >= 0
- ? false
- : !!maskAllText;
- }
- catch (e) {
- }
- return !!maskAllText;
- }
- function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) {
- const win = iframeEl.contentWindow;
- if (!win) {
- return;
- }
- let fired = false;
- let readyState;
- try {
- readyState = win.document.readyState;
- }
- catch (error) {
- return;
- }
- if (readyState !== 'complete') {
- const timer = setTimeout(() => {
- if (!fired) {
- listener();
- fired = true;
- }
- }, iframeLoadTimeout);
- iframeEl.addEventListener('load', () => {
- clearTimeout(timer);
- fired = true;
- listener();
- });
- return;
- }
- const blankUrl = 'about:blank';
- if (win.location.href !== blankUrl ||
- iframeEl.src === blankUrl ||
- iframeEl.src === '') {
- setTimeout(listener, 0);
- return iframeEl.addEventListener('load', listener);
- }
- iframeEl.addEventListener('load', listener);
- }
- function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) {
- let fired = false;
- let styleSheetLoaded;
- try {
- styleSheetLoaded = link.sheet;
- }
- catch (error) {
- return;
- }
- if (styleSheetLoaded)
- return;
- const timer = setTimeout(() => {
- if (!fired) {
- listener();
- fired = true;
- }
- }, styleSheetLoadTimeout);
- link.addEventListener('load', () => {
- clearTimeout(timer);
- fired = true;
- listener();
- });
- }
- function serializeNode(n, options) {
- const { doc, mirror, blockClass, blockSelector, unblockSelector, maskAllText, maskAttributeFn, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options;
- const rootId = getRootId(doc, mirror);
- switch (n.nodeType) {
- case n.DOCUMENT_NODE:
- if (n.compatMode !== 'CSS1Compat') {
- return {
- type: NodeType$1.Document,
- childNodes: [],
- compatMode: n.compatMode,
- };
- }
- else {
- return {
- type: NodeType$1.Document,
- childNodes: [],
- };
- }
- case n.DOCUMENT_TYPE_NODE:
- return {
- type: NodeType$1.DocumentType,
- name: n.name,
- publicId: n.publicId,
- systemId: n.systemId,
- rootId,
- };
- case n.ELEMENT_NODE:
- return serializeElementNode(n, {
- doc,
- blockClass,
- blockSelector,
- unblockSelector,
- inlineStylesheet,
- maskAttributeFn,
- maskInputOptions,
- maskInputFn,
- dataURLOptions,
- inlineImages,
- recordCanvas,
- keepIframeSrcFn,
- newlyAddedElement,
- rootId,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- });
- case n.TEXT_NODE:
- return serializeTextNode(n, {
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- maskTextFn,
- maskInputOptions,
- maskInputFn,
- rootId,
- });
- case n.CDATA_SECTION_NODE:
- return {
- type: NodeType$1.CDATA,
- textContent: '',
- rootId,
- };
- case n.COMMENT_NODE:
- return {
- type: NodeType$1.Comment,
- textContent: n.textContent || '',
- rootId,
- };
- default:
- return false;
- }
- }
- function getRootId(doc, mirror) {
- if (!mirror.hasNode(doc))
- return undefined;
- const docId = mirror.getId(doc);
- return docId === 1 ? undefined : docId;
- }
- function serializeTextNode(n, options) {
- const { maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, maskTextFn, maskInputOptions, maskInputFn, rootId, } = options;
- const parentTagName = n.parentNode && n.parentNode.tagName;
- let textContent = n.textContent;
- const isStyle = parentTagName === 'STYLE' ? true : undefined;
- const isScript = parentTagName === 'SCRIPT' ? true : undefined;
- const isTextarea = parentTagName === 'TEXTAREA' ? true : undefined;
- if (isStyle && textContent) {
- try {
- if (n.nextSibling || n.previousSibling) {
- }
- else if (_optionalChain$5([n, 'access', _6 => _6.parentNode, 'access', _7 => _7.sheet, 'optionalAccess', _8 => _8.cssRules])) {
- textContent = stringifyStylesheet(n.parentNode.sheet);
- }
- }
- catch (err) {
- console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n);
- }
- textContent = absoluteToStylesheet(textContent, getHref());
- }
- if (isScript) {
- textContent = 'SCRIPT_PLACEHOLDER';
- }
- const forceMask = needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText);
- if (!isStyle && !isScript && !isTextarea && textContent && forceMask) {
- textContent = maskTextFn
- ? maskTextFn(textContent)
- : textContent.replace(/[\S]/g, '*');
- }
- if (isTextarea && textContent && (maskInputOptions.textarea || forceMask)) {
- textContent = maskInputFn
- ? maskInputFn(textContent, n.parentNode)
- : textContent.replace(/[\S]/g, '*');
- }
- if (parentTagName === 'OPTION' && textContent) {
- const isInputMasked = shouldMaskInput({
- type: null,
- tagName: parentTagName,
- maskInputOptions,
- });
- textContent = maskInputValue({
- isMasked: needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, isInputMasked),
- element: n,
- value: textContent,
- maskInputFn,
- });
- }
- return {
- type: NodeType$1.Text,
- textContent: textContent || '',
- isStyle,
- rootId,
- };
- }
- function serializeElementNode(n, options) {
- const { doc, blockClass, blockSelector, unblockSelector, inlineStylesheet, maskInputOptions = {}, maskAttributeFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, } = options;
- const needBlock = _isBlockedElement(n, blockClass, blockSelector, unblockSelector);
- const tagName = getValidTagName(n);
- let attributes = {};
- const len = n.attributes.length;
- for (let i = 0; i < len; i++) {
- const attr = n.attributes[i];
- if (attr.name && !ignoreAttribute(tagName, attr.name, attr.value)) {
- attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value, n, maskAttributeFn);
- }
- }
- if (tagName === 'link' && inlineStylesheet) {
- const stylesheet = Array.from(doc.styleSheets).find((s) => {
- return s.href === n.href;
- });
- let cssText = null;
- if (stylesheet) {
- cssText = stringifyStylesheet(stylesheet);
- }
- if (cssText) {
- delete attributes.rel;
- delete attributes.href;
- attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href);
- }
- }
- if (tagName === 'style' &&
- n.sheet &&
- !(n.innerText || n.textContent || '').trim().length) {
- const cssText = stringifyStylesheet(n.sheet);
- if (cssText) {
- attributes._cssText = absoluteToStylesheet(cssText, getHref());
- }
- }
- if (tagName === 'input' ||
- tagName === 'textarea' ||
- tagName === 'select' ||
- tagName === 'option') {
- const el = n;
- const type = getInputType(el);
- const value = getInputValue(el, toUpperCase(tagName), type);
- const checked = el.checked;
- if (type !== 'submit' && type !== 'button' && value) {
- const forceMask = needMaskingText(el, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, shouldMaskInput({
- type,
- tagName: toUpperCase(tagName),
- maskInputOptions,
- }));
- attributes.value = maskInputValue({
- isMasked: forceMask,
- element: el,
- value,
- maskInputFn,
- });
- }
- if (checked) {
- attributes.checked = checked;
- }
- }
- if (tagName === 'option') {
- if (n.selected && !maskInputOptions['select']) {
- attributes.selected = true;
- }
- else {
- delete attributes.selected;
- }
- }
- if (tagName === 'canvas' && recordCanvas) {
- if (n.__context === '2d') {
- if (!is2DCanvasBlank(n)) {
- attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality);
- }
- }
- else if (!('__context' in n)) {
- const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality);
- const blankCanvas = document.createElement('canvas');
- blankCanvas.width = n.width;
- blankCanvas.height = n.height;
- const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality);
- if (canvasDataURL !== blankCanvasDataURL) {
- attributes.rr_dataURL = canvasDataURL;
- }
- }
- }
- if (tagName === 'img' && inlineImages) {
- if (!canvasService) {
- canvasService = doc.createElement('canvas');
- canvasCtx = canvasService.getContext('2d');
- }
- const image = n;
- const oldValue = image.crossOrigin;
- image.crossOrigin = 'anonymous';
- const recordInlineImage = () => {
- image.removeEventListener('load', recordInlineImage);
- try {
- canvasService.width = image.naturalWidth;
- canvasService.height = image.naturalHeight;
- canvasCtx.drawImage(image, 0, 0);
- attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality);
- }
- catch (err) {
- console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`);
- }
- oldValue
- ? (attributes.crossOrigin = oldValue)
- : image.removeAttribute('crossorigin');
- };
- if (image.complete && image.naturalWidth !== 0)
- recordInlineImage();
- else
- image.addEventListener('load', recordInlineImage);
- }
- if (tagName === 'audio' || tagName === 'video') {
- attributes.rr_mediaState = n.paused
- ? 'paused'
- : 'played';
- attributes.rr_mediaCurrentTime = n.currentTime;
- }
- if (!newlyAddedElement) {
- if (n.scrollLeft) {
- attributes.rr_scrollLeft = n.scrollLeft;
- }
- if (n.scrollTop) {
- attributes.rr_scrollTop = n.scrollTop;
- }
- }
- if (needBlock) {
- const { width, height } = n.getBoundingClientRect();
- attributes = {
- class: attributes.class,
- rr_width: `${width}px`,
- rr_height: `${height}px`,
- };
- }
- if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) {
- if (!n.contentDocument) {
- attributes.rr_src = attributes.src;
- }
- delete attributes.src;
- }
- let isCustomElement;
- try {
- if (customElements.get(tagName))
- isCustomElement = true;
- }
- catch (e) {
- }
- return {
- type: NodeType$1.Element,
- tagName,
- attributes,
- childNodes: [],
- isSVG: isSVGElement(n) || undefined,
- needBlock,
- rootId,
- isCustom: isCustomElement,
- };
- }
- function lowerIfExists(maybeAttr) {
- if (maybeAttr === undefined || maybeAttr === null) {
- return '';
- }
- else {
- return maybeAttr.toLowerCase();
- }
- }
- function slimDOMExcluded(sn, slimDOMOptions) {
- if (slimDOMOptions.comment && sn.type === NodeType$1.Comment) {
- return true;
- }
- else if (sn.type === NodeType$1.Element) {
- if (slimDOMOptions.script &&
- (sn.tagName === 'script' ||
- (sn.tagName === 'link' &&
- (sn.attributes.rel === 'preload' ||
- sn.attributes.rel === 'modulepreload') &&
- sn.attributes.as === 'script') ||
- (sn.tagName === 'link' &&
- sn.attributes.rel === 'prefetch' &&
- typeof sn.attributes.href === 'string' &&
- sn.attributes.href.endsWith('.js')))) {
- return true;
- }
- else if (slimDOMOptions.headFavicon &&
- ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') ||
- (sn.tagName === 'meta' &&
- (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) ||
- lowerIfExists(sn.attributes.name) === 'application-name' ||
- lowerIfExists(sn.attributes.rel) === 'icon' ||
- lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' ||
- lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) {
- return true;
- }
- else if (sn.tagName === 'meta') {
- if (slimDOMOptions.headMetaDescKeywords &&
- lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) {
- return true;
- }
- else if (slimDOMOptions.headMetaSocial &&
- (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) ||
- lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) ||
- lowerIfExists(sn.attributes.name) === 'pinterest')) {
- return true;
- }
- else if (slimDOMOptions.headMetaRobots &&
- (lowerIfExists(sn.attributes.name) === 'robots' ||
- lowerIfExists(sn.attributes.name) === 'googlebot' ||
- lowerIfExists(sn.attributes.name) === 'bingbot')) {
- return true;
- }
- else if (slimDOMOptions.headMetaHttpEquiv &&
- sn.attributes['http-equiv'] !== undefined) {
- return true;
- }
- else if (slimDOMOptions.headMetaAuthorship &&
- (lowerIfExists(sn.attributes.name) === 'author' ||
- lowerIfExists(sn.attributes.name) === 'generator' ||
- lowerIfExists(sn.attributes.name) === 'framework' ||
- lowerIfExists(sn.attributes.name) === 'publisher' ||
- lowerIfExists(sn.attributes.name) === 'progid' ||
- lowerIfExists(sn.attributes.property).match(/^article:/) ||
- lowerIfExists(sn.attributes.property).match(/^product:/))) {
- return true;
- }
- else if (slimDOMOptions.headMetaVerification &&
- (lowerIfExists(sn.attributes.name) === 'google-site-verification' ||
- lowerIfExists(sn.attributes.name) === 'yandex-verification' ||
- lowerIfExists(sn.attributes.name) === 'csrf-token' ||
- lowerIfExists(sn.attributes.name) === 'p:domain_verify' ||
- lowerIfExists(sn.attributes.name) === 'verify-v1' ||
- lowerIfExists(sn.attributes.name) === 'verification' ||
- lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) {
- return true;
- }
- }
- }
- return false;
- }
- function serializeNodeWithId(n, options) {
- const { doc, mirror, blockClass, blockSelector, unblockSelector, maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskAttributeFn, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options;
- let { preserveWhiteSpace = true } = options;
- const _serializedNode = serializeNode(n, {
- doc,
- mirror,
- blockClass,
- blockSelector,
- maskAllText,
- unblockSelector,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- inlineStylesheet,
- maskInputOptions,
- maskAttributeFn,
- maskTextFn,
- maskInputFn,
- dataURLOptions,
- inlineImages,
- recordCanvas,
- keepIframeSrcFn,
- newlyAddedElement,
- });
- if (!_serializedNode) {
- console.warn(n, 'not serialized');
- return null;
- }
- let id;
- if (mirror.hasNode(n)) {
- id = mirror.getId(n);
- }
- else if (slimDOMExcluded(_serializedNode, slimDOMOptions) ||
- (!preserveWhiteSpace &&
- _serializedNode.type === NodeType$1.Text &&
- !_serializedNode.isStyle &&
- !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) {
- id = IGNORED_NODE;
- }
- else {
- id = genId();
- }
- const serializedNode = Object.assign(_serializedNode, { id });
- mirror.add(n, serializedNode);
- if (id === IGNORED_NODE) {
- return null;
- }
- if (onSerialize) {
- onSerialize(n);
- }
- let recordChild = !skipChild;
- if (serializedNode.type === NodeType$1.Element) {
- recordChild = recordChild && !serializedNode.needBlock;
- delete serializedNode.needBlock;
- const shadowRoot = n.shadowRoot;
- if (shadowRoot && isNativeShadowDom(shadowRoot))
- serializedNode.isShadowHost = true;
- }
- if ((serializedNode.type === NodeType$1.Document ||
- serializedNode.type === NodeType$1.Element) &&
- recordChild) {
- if (slimDOMOptions.headWhitespace &&
- serializedNode.type === NodeType$1.Element &&
- serializedNode.tagName === 'head') {
- preserveWhiteSpace = false;
- }
- const bypassOptions = {
- doc,
- mirror,
- blockClass,
- blockSelector,
- maskAllText,
- unblockSelector,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- skipChild,
- inlineStylesheet,
- maskInputOptions,
- maskAttributeFn,
- maskTextFn,
- maskInputFn,
- slimDOMOptions,
- dataURLOptions,
- inlineImages,
- recordCanvas,
- preserveWhiteSpace,
- onSerialize,
- onIframeLoad,
- iframeLoadTimeout,
- onStylesheetLoad,
- stylesheetLoadTimeout,
- keepIframeSrcFn,
- };
- for (const childN of Array.from(n.childNodes)) {
- const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
- if (serializedChildNode) {
- serializedNode.childNodes.push(serializedChildNode);
- }
- }
- if (isElement$1(n) && n.shadowRoot) {
- for (const childN of Array.from(n.shadowRoot.childNodes)) {
- const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
- if (serializedChildNode) {
- isNativeShadowDom(n.shadowRoot) &&
- (serializedChildNode.isShadow = true);
- serializedNode.childNodes.push(serializedChildNode);
- }
- }
- }
- }
- if (n.parentNode &&
- isShadowRoot(n.parentNode) &&
- isNativeShadowDom(n.parentNode)) {
- serializedNode.isShadow = true;
- }
- if (serializedNode.type === NodeType$1.Element &&
- serializedNode.tagName === 'iframe') {
- onceIframeLoaded(n, () => {
- const iframeDoc = n.contentDocument;
- if (iframeDoc && onIframeLoad) {
- const serializedIframeNode = serializeNodeWithId(iframeDoc, {
- doc: iframeDoc,
- mirror,
- blockClass,
- blockSelector,
- unblockSelector,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- skipChild: false,
- inlineStylesheet,
- maskInputOptions,
- maskAttributeFn,
- maskTextFn,
- maskInputFn,
- slimDOMOptions,
- dataURLOptions,
- inlineImages,
- recordCanvas,
- preserveWhiteSpace,
- onSerialize,
- onIframeLoad,
- iframeLoadTimeout,
- onStylesheetLoad,
- stylesheetLoadTimeout,
- keepIframeSrcFn,
- });
- if (serializedIframeNode) {
- onIframeLoad(n, serializedIframeNode);
- }
- }
- }, iframeLoadTimeout);
- }
- if (serializedNode.type === NodeType$1.Element &&
- serializedNode.tagName === 'link' &&
- serializedNode.attributes.rel === 'stylesheet') {
- onceStylesheetLoaded(n, () => {
- if (onStylesheetLoad) {
- const serializedLinkNode = serializeNodeWithId(n, {
- doc,
- mirror,
- blockClass,
- blockSelector,
- unblockSelector,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- skipChild: false,
- inlineStylesheet,
- maskInputOptions,
- maskAttributeFn,
- maskTextFn,
- maskInputFn,
- slimDOMOptions,
- dataURLOptions,
- inlineImages,
- recordCanvas,
- preserveWhiteSpace,
- onSerialize,
- onIframeLoad,
- iframeLoadTimeout,
- onStylesheetLoad,
- stylesheetLoadTimeout,
- keepIframeSrcFn,
- });
- if (serializedLinkNode) {
- onStylesheetLoad(n, serializedLinkNode);
- }
- }
- }, stylesheetLoadTimeout);
- }
- return serializedNode;
- }
- function snapshot(n, options) {
- const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, unblockSelector = null, maskAllText = false, maskTextClass = 'rr-mask', unmaskTextClass = null, maskTextSelector = null, unmaskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskAttributeFn, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {};
- const maskInputOptions = maskAllInputs === true
- ? {
- color: true,
- date: true,
- 'datetime-local': true,
- email: true,
- month: true,
- number: true,
- range: true,
- search: true,
- tel: true,
- text: true,
- time: true,
- url: true,
- week: true,
- textarea: true,
- select: true,
- }
- : maskAllInputs === false
- ? {}
- : maskAllInputs;
- const slimDOMOptions = slimDOM === true || slimDOM === 'all'
- ?
- {
- script: true,
- comment: true,
- headFavicon: true,
- headWhitespace: true,
- headMetaDescKeywords: slimDOM === 'all',
- headMetaSocial: true,
- headMetaRobots: true,
- headMetaHttpEquiv: true,
- headMetaAuthorship: true,
- headMetaVerification: true,
- }
- : slimDOM === false
- ? {}
- : slimDOM;
- return serializeNodeWithId(n, {
- doc: n,
- mirror,
- blockClass,
- blockSelector,
- unblockSelector,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- skipChild: false,
- inlineStylesheet,
- maskInputOptions,
- maskAttributeFn,
- maskTextFn,
- maskInputFn,
- slimDOMOptions,
- dataURLOptions,
- inlineImages,
- recordCanvas,
- preserveWhiteSpace,
- onSerialize,
- onIframeLoad,
- iframeLoadTimeout,
- onStylesheetLoad,
- stylesheetLoadTimeout,
- keepIframeSrcFn,
- newlyAddedElement: false,
- });
- }
- function _optionalChain$4(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
- function on(type, fn, target = document) {
- const options = { capture: true, passive: true };
- target.addEventListener(type, fn, options);
- return () => target.removeEventListener(type, fn, options);
- }
- const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' +
- '\r\n' +
- 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' +
- '\r\n' +
- 'or you can use record.mirror to access the mirror instance during recording.';
- let _mirror = {
- map: {},
- getId() {
- console.error(DEPARTED_MIRROR_ACCESS_WARNING);
- return -1;
- },
- getNode() {
- console.error(DEPARTED_MIRROR_ACCESS_WARNING);
- return null;
- },
- removeNodeFromMap() {
- console.error(DEPARTED_MIRROR_ACCESS_WARNING);
- },
- has() {
- console.error(DEPARTED_MIRROR_ACCESS_WARNING);
- return false;
- },
- reset() {
- console.error(DEPARTED_MIRROR_ACCESS_WARNING);
- },
- };
- if (typeof window !== 'undefined' && window.Proxy && window.Reflect) {
- _mirror = new Proxy(_mirror, {
- get(target, prop, receiver) {
- if (prop === 'map') {
- console.error(DEPARTED_MIRROR_ACCESS_WARNING);
- }
- return Reflect.get(target, prop, receiver);
- },
- });
- }
- function throttle$1(func, wait, options = {}) {
- let timeout = null;
- let previous = 0;
- return function (...args) {
- const now = Date.now();
- if (!previous && options.leading === false) {
- previous = now;
- }
- const remaining = wait - (now - previous);
- const context = this;
- if (remaining <= 0 || remaining > wait) {
- if (timeout) {
- clearTimeout(timeout);
- timeout = null;
- }
- previous = now;
- func.apply(context, args);
- }
- else if (!timeout && options.trailing !== false) {
- timeout = setTimeout(() => {
- previous = options.leading === false ? 0 : Date.now();
- timeout = null;
- func.apply(context, args);
- }, remaining);
- }
- };
- }
- function hookSetter(target, key, d, isRevoked, win = window) {
- const original = win.Object.getOwnPropertyDescriptor(target, key);
- win.Object.defineProperty(target, key, isRevoked
- ? d
- : {
- set(value) {
- setTimeout(() => {
- d.set.call(this, value);
- }, 0);
- if (original && original.set) {
- original.set.call(this, value);
- }
- },
- });
- return () => hookSetter(target, key, original || {}, true);
- }
- function patch(source, name, replacement) {
- try {
- if (!(name in source)) {
- return () => {
- };
- }
- const original = source[name];
- const wrapped = replacement(original);
- if (typeof wrapped === 'function') {
- wrapped.prototype = wrapped.prototype || {};
- Object.defineProperties(wrapped, {
- __rrweb_original__: {
- enumerable: false,
- value: original,
- },
- });
- }
- source[name] = wrapped;
- return () => {
- source[name] = original;
- };
- }
- catch (e2) {
- return () => {
- };
- }
- }
- let nowTimestamp = Date.now;
- if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) {
- nowTimestamp = () => new Date().getTime();
- }
- function getWindowScroll(win) {
- const doc = win.document;
- return {
- left: doc.scrollingElement
- ? doc.scrollingElement.scrollLeft
- : win.pageXOffset !== undefined
- ? win.pageXOffset
- : _optionalChain$4([doc, 'optionalAccess', _ => _.documentElement, 'access', _2 => _2.scrollLeft]) ||
- _optionalChain$4([doc, 'optionalAccess', _3 => _3.body, 'optionalAccess', _4 => _4.parentElement, 'optionalAccess', _5 => _5.scrollLeft]) ||
- _optionalChain$4([doc, 'optionalAccess', _6 => _6.body, 'optionalAccess', _7 => _7.scrollLeft]) ||
- 0,
- top: doc.scrollingElement
- ? doc.scrollingElement.scrollTop
- : win.pageYOffset !== undefined
- ? win.pageYOffset
- : _optionalChain$4([doc, 'optionalAccess', _8 => _8.documentElement, 'access', _9 => _9.scrollTop]) ||
- _optionalChain$4([doc, 'optionalAccess', _10 => _10.body, 'optionalAccess', _11 => _11.parentElement, 'optionalAccess', _12 => _12.scrollTop]) ||
- _optionalChain$4([doc, 'optionalAccess', _13 => _13.body, 'optionalAccess', _14 => _14.scrollTop]) ||
- 0,
- };
- }
- function getWindowHeight() {
- return (window.innerHeight ||
- (document.documentElement && document.documentElement.clientHeight) ||
- (document.body && document.body.clientHeight));
- }
- function getWindowWidth() {
- return (window.innerWidth ||
- (document.documentElement && document.documentElement.clientWidth) ||
- (document.body && document.body.clientWidth));
- }
- function isBlocked(node, blockClass, blockSelector, unblockSelector, checkAncestors) {
- if (!node) {
- return false;
- }
- const el = node.nodeType === node.ELEMENT_NODE
- ? node
- : node.parentElement;
- if (!el)
- return false;
- const blockedPredicate = createMatchPredicate(blockClass, blockSelector);
- if (!checkAncestors) {
- const isUnblocked = unblockSelector && el.matches(unblockSelector);
- return blockedPredicate(el) && !isUnblocked;
- }
- const blockDistance = distanceToMatch(el, blockedPredicate);
- let unblockDistance = -1;
- if (blockDistance < 0) {
- return false;
- }
- if (unblockSelector) {
- unblockDistance = distanceToMatch(el, createMatchPredicate(null, unblockSelector));
- }
- if (blockDistance > -1 && unblockDistance < 0) {
- return true;
- }
- return blockDistance < unblockDistance;
- }
- function isSerialized(n, mirror) {
- return mirror.getId(n) !== -1;
- }
- function isIgnored(n, mirror) {
- return mirror.getId(n) === IGNORED_NODE;
- }
- function isAncestorRemoved(target, mirror) {
- if (isShadowRoot(target)) {
- return false;
- }
- const id = mirror.getId(target);
- if (!mirror.has(id)) {
- return true;
- }
- if (target.parentNode &&
- target.parentNode.nodeType === target.DOCUMENT_NODE) {
- return false;
- }
- if (!target.parentNode) {
- return true;
- }
- return isAncestorRemoved(target.parentNode, mirror);
- }
- function legacy_isTouchEvent(event) {
- return Boolean(event.changedTouches);
- }
- function polyfill(win = window) {
- if ('NodeList' in win && !win.NodeList.prototype.forEach) {
- win.NodeList.prototype.forEach = Array.prototype
- .forEach;
- }
- if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) {
- win.DOMTokenList.prototype.forEach = Array.prototype
- .forEach;
- }
- if (!Node.prototype.contains) {
- Node.prototype.contains = (...args) => {
- let node = args[0];
- if (!(0 in args)) {
- throw new TypeError('1 argument is required');
- }
- do {
- if (this === node) {
- return true;
- }
- } while ((node = node && node.parentNode));
- return false;
- };
- }
- }
- function isSerializedIframe(n, mirror) {
- return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
- }
- function isSerializedStylesheet(n, mirror) {
- return Boolean(n.nodeName === 'LINK' &&
- n.nodeType === n.ELEMENT_NODE &&
- n.getAttribute &&
- n.getAttribute('rel') === 'stylesheet' &&
- mirror.getMeta(n));
- }
- function hasShadowRoot(n) {
- return Boolean(_optionalChain$4([n, 'optionalAccess', _18 => _18.shadowRoot]));
- }
- class StyleSheetMirror {
- constructor() {
- this.id = 1;
- this.styleIDMap = new WeakMap();
- this.idStyleMap = new Map();
- }
- getId(stylesheet) {
- return _nullishCoalesce(this.styleIDMap.get(stylesheet), () => ( -1));
- }
- has(stylesheet) {
- return this.styleIDMap.has(stylesheet);
- }
- add(stylesheet, id) {
- if (this.has(stylesheet))
- return this.getId(stylesheet);
- let newId;
- if (id === undefined) {
- newId = this.id++;
- }
- else
- newId = id;
- this.styleIDMap.set(stylesheet, newId);
- this.idStyleMap.set(newId, stylesheet);
- return newId;
- }
- getStyle(id) {
- return this.idStyleMap.get(id) || null;
- }
- reset() {
- this.styleIDMap = new WeakMap();
- this.idStyleMap = new Map();
- this.id = 1;
- }
- generateId() {
- return this.id++;
- }
- }
- function getShadowHost(n) {
- let shadowHost = null;
- if (_optionalChain$4([n, 'access', _19 => _19.getRootNode, 'optionalCall', _20 => _20(), 'optionalAccess', _21 => _21.nodeType]) === Node.DOCUMENT_FRAGMENT_NODE &&
- n.getRootNode().host)
- shadowHost = n.getRootNode().host;
- return shadowHost;
- }
- function getRootShadowHost(n) {
- let rootShadowHost = n;
- let shadowHost;
- while ((shadowHost = getShadowHost(rootShadowHost)))
- rootShadowHost = shadowHost;
- return rootShadowHost;
- }
- function shadowHostInDom(n) {
- const doc = n.ownerDocument;
- if (!doc)
- return false;
- const shadowHost = getRootShadowHost(n);
- return doc.contains(shadowHost);
- }
- function inDom(n) {
- const doc = n.ownerDocument;
- if (!doc)
- return false;
- return doc.contains(n) || shadowHostInDom(n);
- }
- let cachedRequestAnimationFrameImplementation;
- function getRequestAnimationFrameImplementation() {
- if (cachedRequestAnimationFrameImplementation) {
- return cachedRequestAnimationFrameImplementation;
- }
- const document = window.document;
- let requestAnimationFrameImplementation = window.requestAnimationFrame;
- if (document && typeof document.createElement === 'function') {
- try {
- const sandbox = document.createElement('iframe');
- sandbox.hidden = true;
- document.head.appendChild(sandbox);
- const contentWindow = sandbox.contentWindow;
- if (contentWindow && contentWindow.requestAnimationFrame) {
- requestAnimationFrameImplementation =
- contentWindow.requestAnimationFrame;
- }
- document.head.removeChild(sandbox);
- }
- catch (e) {
- }
- }
- return (cachedRequestAnimationFrameImplementation =
- requestAnimationFrameImplementation.bind(window));
- }
- function onRequestAnimationFrame(...rest) {
- return getRequestAnimationFrameImplementation()(...rest);
- }
- var EventType = /* @__PURE__ */ ((EventType2) => {
- EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
- EventType2[EventType2["Load"] = 1] = "Load";
- EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
- EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
- EventType2[EventType2["Meta"] = 4] = "Meta";
- EventType2[EventType2["Custom"] = 5] = "Custom";
- EventType2[EventType2["Plugin"] = 6] = "Plugin";
- return EventType2;
- })(EventType || {});
- var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
- IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
- IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
- IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
- IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
- IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
- IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
- IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
- IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
- IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
- IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
- IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
- IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
- IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
- IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
- IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
- IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
- IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
- return IncrementalSource2;
- })(IncrementalSource || {});
- var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => {
- MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp";
- MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown";
- MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click";
- MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu";
- MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick";
- MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus";
- MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur";
- MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart";
- MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed";
- MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd";
- MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel";
- return MouseInteractions2;
- })(MouseInteractions || {});
- var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => {
- PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse";
- PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen";
- PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch";
- return PointerTypes2;
- })(PointerTypes || {});
- function _optionalChain$3(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
- function isNodeInLinkedList(n) {
- return '__ln' in n;
- }
- class DoubleLinkedList {
- constructor() {
- this.length = 0;
- this.head = null;
- this.tail = null;
- }
- get(position) {
- if (position >= this.length) {
- throw new Error('Position outside of list range');
- }
- let current = this.head;
- for (let index = 0; index < position; index++) {
- current = _optionalChain$3([current, 'optionalAccess', _ => _.next]) || null;
- }
- return current;
- }
- addNode(n) {
- const node = {
- value: n,
- previous: null,
- next: null,
- };
- n.__ln = node;
- if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) {
- const current = n.previousSibling.__ln.next;
- node.next = current;
- node.previous = n.previousSibling.__ln;
- n.previousSibling.__ln.next = node;
- if (current) {
- current.previous = node;
- }
- }
- else if (n.nextSibling &&
- isNodeInLinkedList(n.nextSibling) &&
- n.nextSibling.__ln.previous) {
- const current = n.nextSibling.__ln.previous;
- node.previous = current;
- node.next = n.nextSibling.__ln;
- n.nextSibling.__ln.previous = node;
- if (current) {
- current.next = node;
- }
- }
- else {
- if (this.head) {
- this.head.previous = node;
- }
- node.next = this.head;
- this.head = node;
- }
- if (node.next === null) {
- this.tail = node;
- }
- this.length++;
- }
- removeNode(n) {
- const current = n.__ln;
- if (!this.head) {
- return;
- }
- if (!current.previous) {
- this.head = current.next;
- if (this.head) {
- this.head.previous = null;
- }
- else {
- this.tail = null;
- }
- }
- else {
- current.previous.next = current.next;
- if (current.next) {
- current.next.previous = current.previous;
- }
- else {
- this.tail = current.previous;
- }
- }
- if (n.__ln) {
- delete n.__ln;
- }
- this.length--;
- }
- }
- const moveKey = (id, parentId) => `${id}@${parentId}`;
- class MutationBuffer {
- constructor() {
- this.frozen = false;
- this.locked = false;
- this.texts = [];
- this.attributes = [];
- this.removes = [];
- this.mapRemoves = [];
- this.movedMap = {};
- this.addedSet = new Set();
- this.movedSet = new Set();
- this.droppedSet = new Set();
- this.processMutations = (mutations) => {
- mutations.forEach(this.processMutation);
- this.emit();
- };
- this.emit = () => {
- if (this.frozen || this.locked) {
- return;
- }
- const adds = [];
- const addedIds = new Set();
- const addList = new DoubleLinkedList();
- const getNextId = (n) => {
- let ns = n;
- let nextId = IGNORED_NODE;
- while (nextId === IGNORED_NODE) {
- ns = ns && ns.nextSibling;
- nextId = ns && this.mirror.getId(ns);
- }
- return nextId;
- };
- const pushAdd = (n) => {
- if (!n.parentNode || !inDom(n)) {
- return;
- }
- const parentId = isShadowRoot(n.parentNode)
- ? this.mirror.getId(getShadowHost(n))
- : this.mirror.getId(n.parentNode);
- const nextId = getNextId(n);
- if (parentId === -1 || nextId === -1) {
- return addList.addNode(n);
- }
- const sn = serializeNodeWithId(n, {
- doc: this.doc,
- mirror: this.mirror,
- blockClass: this.blockClass,
- blockSelector: this.blockSelector,
- maskAllText: this.maskAllText,
- unblockSelector: this.unblockSelector,
- maskTextClass: this.maskTextClass,
- unmaskTextClass: this.unmaskTextClass,
- maskTextSelector: this.maskTextSelector,
- unmaskTextSelector: this.unmaskTextSelector,
- skipChild: true,
- newlyAddedElement: true,
- inlineStylesheet: this.inlineStylesheet,
- maskInputOptions: this.maskInputOptions,
- maskAttributeFn: this.maskAttributeFn,
- maskTextFn: this.maskTextFn,
- maskInputFn: this.maskInputFn,
- slimDOMOptions: this.slimDOMOptions,
- dataURLOptions: this.dataURLOptions,
- recordCanvas: this.recordCanvas,
- inlineImages: this.inlineImages,
- onSerialize: (currentN) => {
- if (isSerializedIframe(currentN, this.mirror)) {
- this.iframeManager.addIframe(currentN);
- }
- if (isSerializedStylesheet(currentN, this.mirror)) {
- this.stylesheetManager.trackLinkElement(currentN);
- }
- if (hasShadowRoot(n)) {
- this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc);
- }
- },
- onIframeLoad: (iframe, childSn) => {
- this.iframeManager.attachIframe(iframe, childSn);
- this.shadowDomManager.observeAttachShadow(iframe);
- },
- onStylesheetLoad: (link, childSn) => {
- this.stylesheetManager.attachLinkElement(link, childSn);
- },
- });
- if (sn) {
- adds.push({
- parentId,
- nextId,
- node: sn,
- });
- addedIds.add(sn.id);
- }
- };
- while (this.mapRemoves.length) {
- this.mirror.removeNodeFromMap(this.mapRemoves.shift());
- }
- for (const n of this.movedSet) {
- if (isParentRemoved(this.removes, n, this.mirror) &&
- !this.movedSet.has(n.parentNode)) {
- continue;
- }
- pushAdd(n);
- }
- for (const n of this.addedSet) {
- if (!isAncestorInSet(this.droppedSet, n) &&
- !isParentRemoved(this.removes, n, this.mirror)) {
- pushAdd(n);
- }
- else if (isAncestorInSet(this.movedSet, n)) {
- pushAdd(n);
- }
- else {
- this.droppedSet.add(n);
- }
- }
- let candidate = null;
- while (addList.length) {
- let node = null;
- if (candidate) {
- const parentId = this.mirror.getId(candidate.value.parentNode);
- const nextId = getNextId(candidate.value);
- if (parentId !== -1 && nextId !== -1) {
- node = candidate;
- }
- }
- if (!node) {
- let tailNode = addList.tail;
- while (tailNode) {
- const _node = tailNode;
- tailNode = tailNode.previous;
- if (_node) {
- const parentId = this.mirror.getId(_node.value.parentNode);
- const nextId = getNextId(_node.value);
- if (nextId === -1)
- continue;
- else if (parentId !== -1) {
- node = _node;
- break;
- }
- else {
- const unhandledNode = _node.value;
- if (unhandledNode.parentNode &&
- unhandledNode.parentNode.nodeType ===
- Node.DOCUMENT_FRAGMENT_NODE) {
- const shadowHost = unhandledNode.parentNode
- .host;
- const parentId = this.mirror.getId(shadowHost);
- if (parentId !== -1) {
- node = _node;
- break;
- }
- }
- }
- }
- }
- }
- if (!node) {
- while (addList.head) {
- addList.removeNode(addList.head.value);
- }
- break;
- }
- candidate = node.previous;
- addList.removeNode(node.value);
- pushAdd(node.value);
- }
- const payload = {
- texts: this.texts
- .map((text) => ({
- id: this.mirror.getId(text.node),
- value: text.value,
- }))
- .filter((text) => !addedIds.has(text.id))
- .filter((text) => this.mirror.has(text.id)),
- attributes: this.attributes
- .map((attribute) => {
- const { attributes } = attribute;
- if (typeof attributes.style === 'string') {
- const diffAsStr = JSON.stringify(attribute.styleDiff);
- const unchangedAsStr = JSON.stringify(attribute._unchangedStyles);
- if (diffAsStr.length < attributes.style.length) {
- if ((diffAsStr + unchangedAsStr).split('var(').length ===
- attributes.style.split('var(').length) {
- attributes.style = attribute.styleDiff;
- }
- }
- }
- return {
- id: this.mirror.getId(attribute.node),
- attributes: attributes,
- };
- })
- .filter((attribute) => !addedIds.has(attribute.id))
- .filter((attribute) => this.mirror.has(attribute.id)),
- removes: this.removes,
- adds,
- };
- if (!payload.texts.length &&
- !payload.attributes.length &&
- !payload.removes.length &&
- !payload.adds.length) {
- return;
- }
- this.texts = [];
- this.attributes = [];
- this.removes = [];
- this.addedSet = new Set();
- this.movedSet = new Set();
- this.droppedSet = new Set();
- this.movedMap = {};
- this.mutationCb(payload);
- };
- this.processMutation = (m) => {
- if (isIgnored(m.target, this.mirror)) {
- return;
- }
- let unattachedDoc;
- try {
- unattachedDoc = document.implementation.createHTMLDocument();
- }
- catch (e) {
- unattachedDoc = this.doc;
- }
- switch (m.type) {
- case 'characterData': {
- const value = m.target.textContent;
- if (!isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) &&
- value !== m.oldValue) {
- this.texts.push({
- value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextClass, this.unmaskTextSelector, this.maskAllText) && value
- ? this.maskTextFn
- ? this.maskTextFn(value)
- : value.replace(/[\S]/g, '*')
- : value,
- node: m.target,
- });
- }
- break;
- }
- case 'attributes': {
- const target = m.target;
- let attributeName = m.attributeName;
- let value = m.target.getAttribute(attributeName);
- if (attributeName === 'value') {
- const type = getInputType(target);
- const tagName = target.tagName;
- value = getInputValue(target, tagName, type);
- const isInputMasked = shouldMaskInput({
- maskInputOptions: this.maskInputOptions,
- tagName,
- type,
- });
- const forceMask = needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextClass, this.unmaskTextSelector, isInputMasked);
- value = maskInputValue({
- isMasked: forceMask,
- element: target,
- value,
- maskInputFn: this.maskInputFn,
- });
- }
- if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) ||
- value === m.oldValue) {
- return;
- }
- let item = this.attributes.find((a) => a.node === m.target);
- if (target.tagName === 'IFRAME' &&
- attributeName === 'src' &&
- !this.keepIframeSrcFn(value)) {
- if (!target.contentDocument) {
- attributeName = 'rr_src';
- }
- else {
- return;
- }
- }
- if (!item) {
- item = {
- node: m.target,
- attributes: {},
- styleDiff: {},
- _unchangedStyles: {},
- };
- this.attributes.push(item);
- }
- if (attributeName === 'type' &&
- target.tagName === 'INPUT' &&
- (m.oldValue || '').toLowerCase() === 'password') {
- target.setAttribute('data-rr-is-password', 'true');
- }
- if (!ignoreAttribute(target.tagName, attributeName)) {
- item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value, target, this.maskAttributeFn);
- if (attributeName === 'style') {
- const old = unattachedDoc.createElement('span');
- if (m.oldValue) {
- old.setAttribute('style', m.oldValue);
- }
- for (const pname of Array.from(target.style)) {
- const newValue = target.style.getPropertyValue(pname);
- const newPriority = target.style.getPropertyPriority(pname);
- if (newValue !== old.style.getPropertyValue(pname) ||
- newPriority !== old.style.getPropertyPriority(pname)) {
- if (newPriority === '') {
- item.styleDiff[pname] = newValue;
- }
- else {
- item.styleDiff[pname] = [newValue, newPriority];
- }
- }
- else {
- item._unchangedStyles[pname] = [newValue, newPriority];
- }
- }
- for (const pname of Array.from(old.style)) {
- if (target.style.getPropertyValue(pname) === '') {
- item.styleDiff[pname] = false;
- }
- }
- }
- }
- break;
- }
- case 'childList': {
- if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, true)) {
- return;
- }
- m.addedNodes.forEach((n) => this.genAdds(n, m.target));
- m.removedNodes.forEach((n) => {
- const nodeId = this.mirror.getId(n);
- const parentId = isShadowRoot(m.target)
- ? this.mirror.getId(m.target.host)
- : this.mirror.getId(m.target);
- if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) ||
- isIgnored(n, this.mirror) ||
- !isSerialized(n, this.mirror)) {
- return;
- }
- if (this.addedSet.has(n)) {
- deepDelete(this.addedSet, n);
- this.droppedSet.add(n);
- }
- else if (this.addedSet.has(m.target) && nodeId === -1) ;
- else if (isAncestorRemoved(m.target, this.mirror)) ;
- else if (this.movedSet.has(n) &&
- this.movedMap[moveKey(nodeId, parentId)]) {
- deepDelete(this.movedSet, n);
- }
- else {
- this.removes.push({
- parentId,
- id: nodeId,
- isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target)
- ? true
- : undefined,
- });
- }
- this.mapRemoves.push(n);
- });
- break;
- }
- }
- };
- this.genAdds = (n, target) => {
- if (this.processedNodeManager.inOtherBuffer(n, this))
- return;
- if (this.addedSet.has(n) || this.movedSet.has(n))
- return;
- if (this.mirror.hasNode(n)) {
- if (isIgnored(n, this.mirror)) {
- return;
- }
- this.movedSet.add(n);
- let targetId = null;
- if (target && this.mirror.hasNode(target)) {
- targetId = this.mirror.getId(target);
- }
- if (targetId && targetId !== -1) {
- this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true;
- }
- }
- else {
- this.addedSet.add(n);
- this.droppedSet.delete(n);
- }
- if (!isBlocked(n, this.blockClass, this.blockSelector, this.unblockSelector, false)) {
- n.childNodes.forEach((childN) => this.genAdds(childN));
- if (hasShadowRoot(n)) {
- n.shadowRoot.childNodes.forEach((childN) => {
- this.processedNodeManager.add(childN, this);
- this.genAdds(childN, n);
- });
- }
- }
- };
- }
- init(options) {
- [
- 'mutationCb',
- 'blockClass',
- 'blockSelector',
- 'unblockSelector',
- 'maskAllText',
- 'maskTextClass',
- 'unmaskTextClass',
- 'maskTextSelector',
- 'unmaskTextSelector',
- 'inlineStylesheet',
- 'maskInputOptions',
- 'maskAttributeFn',
- 'maskTextFn',
- 'maskInputFn',
- 'keepIframeSrcFn',
- 'recordCanvas',
- 'inlineImages',
- 'slimDOMOptions',
- 'dataURLOptions',
- 'doc',
- 'mirror',
- 'iframeManager',
- 'stylesheetManager',
- 'shadowDomManager',
- 'canvasManager',
- 'processedNodeManager',
- ].forEach((key) => {
- this[key] = options[key];
- });
- }
- freeze() {
- this.frozen = true;
- this.canvasManager.freeze();
- }
- unfreeze() {
- this.frozen = false;
- this.canvasManager.unfreeze();
- this.emit();
- }
- isFrozen() {
- return this.frozen;
- }
- lock() {
- this.locked = true;
- this.canvasManager.lock();
- }
- unlock() {
- this.locked = false;
- this.canvasManager.unlock();
- this.emit();
- }
- reset() {
- this.shadowDomManager.reset();
- this.canvasManager.reset();
- }
- }
- function deepDelete(addsSet, n) {
- addsSet.delete(n);
- n.childNodes.forEach((childN) => deepDelete(addsSet, childN));
- }
- function isParentRemoved(removes, n, mirror) {
- if (removes.length === 0)
- return false;
- return _isParentRemoved(removes, n, mirror);
- }
- function _isParentRemoved(removes, n, mirror) {
- const { parentNode } = n;
- if (!parentNode) {
- return false;
- }
- const parentId = mirror.getId(parentNode);
- if (removes.some((r) => r.id === parentId)) {
- return true;
- }
- return _isParentRemoved(removes, parentNode, mirror);
- }
- function isAncestorInSet(set, n) {
- if (set.size === 0)
- return false;
- return _isAncestorInSet(set, n);
- }
- function _isAncestorInSet(set, n) {
- const { parentNode } = n;
- if (!parentNode) {
- return false;
- }
- if (set.has(parentNode)) {
- return true;
- }
- return _isAncestorInSet(set, parentNode);
- }
- let errorHandler;
- function registerErrorHandler(handler) {
- errorHandler = handler;
- }
- function unregisterErrorHandler() {
- errorHandler = undefined;
- }
- const callbackWrapper = (cb) => {
- if (!errorHandler) {
- return cb;
- }
- const rrwebWrapped = ((...rest) => {
- try {
- return cb(...rest);
- }
- catch (error) {
- if (errorHandler && errorHandler(error) === true) {
- return () => {
- };
- }
- throw error;
- }
- });
- return rrwebWrapped;
- };
- function _optionalChain$2(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
- const mutationBuffers = [];
- function getEventTarget(event) {
- try {
- if ('composedPath' in event) {
- const path = event.composedPath();
- if (path.length) {
- return path[0];
- }
- }
- else if ('path' in event && event.path.length) {
- return event.path[0];
- }
- }
- catch (e2) {
- }
- return event && event.target;
- }
- function initMutationObserver(options, rootEl) {
- const mutationBuffer = new MutationBuffer();
- mutationBuffers.push(mutationBuffer);
- mutationBuffer.init(options);
- let mutationObserverCtor = window.MutationObserver ||
- window.__rrMutationObserver;
- const angularZoneSymbol = _optionalChain$2([window, 'optionalAccess', _ => _.Zone, 'optionalAccess', _2 => _2.__symbol__, 'optionalCall', _3 => _3('MutationObserver')]);
- if (angularZoneSymbol &&
- window[angularZoneSymbol]) {
- mutationObserverCtor = window[angularZoneSymbol];
- }
- const observer = new mutationObserverCtor(callbackWrapper((mutations) => {
- if (options.onMutation && options.onMutation(mutations) === false) {
- return;
- }
- mutationBuffer.processMutations.bind(mutationBuffer)(mutations);
- }));
- observer.observe(rootEl, {
- attributes: true,
- attributeOldValue: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true,
- });
- return observer;
- }
- function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) {
- if (sampling.mousemove === false) {
- return () => {
- };
- }
- const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50;
- const callbackThreshold = typeof sampling.mousemoveCallback === 'number'
- ? sampling.mousemoveCallback
- : 500;
- let positions = [];
- let timeBaseline;
- const wrappedCb = throttle$1(callbackWrapper((source) => {
- const totalOffset = Date.now() - timeBaseline;
- mousemoveCb(positions.map((p) => {
- p.timeOffset -= totalOffset;
- return p;
- }), source);
- positions = [];
- timeBaseline = null;
- }), callbackThreshold);
- const updatePosition = callbackWrapper(throttle$1(callbackWrapper((evt) => {
- const target = getEventTarget(evt);
- const { clientX, clientY } = legacy_isTouchEvent(evt)
- ? evt.changedTouches[0]
- : evt;
- if (!timeBaseline) {
- timeBaseline = nowTimestamp();
- }
- positions.push({
- x: clientX,
- y: clientY,
- id: mirror.getId(target),
- timeOffset: nowTimestamp() - timeBaseline,
- });
- wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent
- ? IncrementalSource.Drag
- : evt instanceof MouseEvent
- ? IncrementalSource.MouseMove
- : IncrementalSource.TouchMove);
- }), threshold, {
- trailing: false,
- }));
- const handlers = [
- on('mousemove', updatePosition, doc),
- on('touchmove', updatePosition, doc),
- on('drag', updatePosition, doc),
- ];
- return callbackWrapper(() => {
- handlers.forEach((h) => h());
- });
- }
- function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) {
- if (sampling.mouseInteraction === false) {
- return () => {
- };
- }
- const disableMap = sampling.mouseInteraction === true ||
- sampling.mouseInteraction === undefined
- ? {}
- : sampling.mouseInteraction;
- const handlers = [];
- let currentPointerType = null;
- const getHandler = (eventKey) => {
- return (event) => {
- const target = getEventTarget(event);
- if (isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
- return;
- }
- let pointerType = null;
- let thisEventKey = eventKey;
- if ('pointerType' in event) {
- switch (event.pointerType) {
- case 'mouse':
- pointerType = PointerTypes.Mouse;
- break;
- case 'touch':
- pointerType = PointerTypes.Touch;
- break;
- case 'pen':
- pointerType = PointerTypes.Pen;
- break;
- }
- if (pointerType === PointerTypes.Touch) {
- if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) {
- thisEventKey = 'TouchStart';
- }
- else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) {
- thisEventKey = 'TouchEnd';
- }
- }
- else if (pointerType === PointerTypes.Pen) ;
- }
- else if (legacy_isTouchEvent(event)) {
- pointerType = PointerTypes.Touch;
- }
- if (pointerType !== null) {
- currentPointerType = pointerType;
- if ((thisEventKey.startsWith('Touch') &&
- pointerType === PointerTypes.Touch) ||
- (thisEventKey.startsWith('Mouse') &&
- pointerType === PointerTypes.Mouse)) {
- pointerType = null;
- }
- }
- else if (MouseInteractions[eventKey] === MouseInteractions.Click) {
- pointerType = currentPointerType;
- currentPointerType = null;
- }
- const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event;
- if (!e) {
- return;
- }
- const id = mirror.getId(target);
- const { clientX, clientY } = e;
- callbackWrapper(mouseInteractionCb)({
- type: MouseInteractions[thisEventKey],
- id,
- x: clientX,
- y: clientY,
- ...(pointerType !== null && { pointerType }),
- });
- };
- };
- Object.keys(MouseInteractions)
- .filter((key) => Number.isNaN(Number(key)) &&
- !key.endsWith('_Departed') &&
- disableMap[key] !== false)
- .forEach((eventKey) => {
- let eventName = toLowerCase(eventKey);
- const handler = getHandler(eventKey);
- if (window.PointerEvent) {
- switch (MouseInteractions[eventKey]) {
- case MouseInteractions.MouseDown:
- case MouseInteractions.MouseUp:
- eventName = eventName.replace('mouse', 'pointer');
- break;
- case MouseInteractions.TouchStart:
- case MouseInteractions.TouchEnd:
- return;
- }
- }
- handlers.push(on(eventName, handler, doc));
- });
- return callbackWrapper(() => {
- handlers.forEach((h) => h());
- });
- }
- function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) {
- const updatePosition = callbackWrapper(throttle$1(callbackWrapper((evt) => {
- const target = getEventTarget(evt);
- if (!target ||
- isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
- return;
- }
- const id = mirror.getId(target);
- if (target === doc && doc.defaultView) {
- const scrollLeftTop = getWindowScroll(doc.defaultView);
- scrollCb({
- id,
- x: scrollLeftTop.left,
- y: scrollLeftTop.top,
- });
- }
- else {
- scrollCb({
- id,
- x: target.scrollLeft,
- y: target.scrollTop,
- });
- }
- }), sampling.scroll || 100));
- return on('scroll', updatePosition, doc);
- }
- function initViewportResizeObserver({ viewportResizeCb }, { win }) {
- let lastH = -1;
- let lastW = -1;
- const updateDimension = callbackWrapper(throttle$1(callbackWrapper(() => {
- const height = getWindowHeight();
- const width = getWindowWidth();
- if (lastH !== height || lastW !== width) {
- viewportResizeCb({
- width: Number(width),
- height: Number(height),
- });
- lastH = height;
- lastW = width;
- }
- }), 200));
- return on('resize', updateDimension, win);
- }
- const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
- const lastInputValueMap = new WeakMap();
- function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, unblockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, }) {
- function eventHandler(event) {
- let target = getEventTarget(event);
- const userTriggered = event.isTrusted;
- const tagName = target && toUpperCase(target.tagName);
- if (tagName === 'OPTION')
- target = target.parentElement;
- if (!target ||
- !tagName ||
- INPUT_TAGS.indexOf(tagName) < 0 ||
- isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
- return;
- }
- const el = target;
- if (el.classList.contains(ignoreClass) ||
- (ignoreSelector && el.matches(ignoreSelector))) {
- return;
- }
- const type = getInputType(target);
- let text = getInputValue(el, tagName, type);
- let isChecked = false;
- const isInputMasked = shouldMaskInput({
- maskInputOptions,
- tagName,
- type,
- });
- const forceMask = needMaskingText(target, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, isInputMasked);
- if (type === 'radio' || type === 'checkbox') {
- isChecked = target.checked;
- }
- text = maskInputValue({
- isMasked: forceMask,
- element: target,
- value: text,
- maskInputFn,
- });
- cbWithDedup(target, userTriggeredOnInput
- ? { text, isChecked, userTriggered }
- : { text, isChecked });
- const name = target.name;
- if (type === 'radio' && name && isChecked) {
- doc
- .querySelectorAll(`input[type="radio"][name="${name}"]`)
- .forEach((el) => {
- if (el !== target) {
- const text = maskInputValue({
- isMasked: forceMask,
- element: el,
- value: getInputValue(el, tagName, type),
- maskInputFn,
- });
- cbWithDedup(el, userTriggeredOnInput
- ? { text, isChecked: !isChecked, userTriggered: false }
- : { text, isChecked: !isChecked });
- }
- });
- }
- }
- function cbWithDedup(target, v) {
- const lastInputValue = lastInputValueMap.get(target);
- if (!lastInputValue ||
- lastInputValue.text !== v.text ||
- lastInputValue.isChecked !== v.isChecked) {
- lastInputValueMap.set(target, v);
- const id = mirror.getId(target);
- callbackWrapper(inputCb)({
- ...v,
- id,
- });
- }
- }
- const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
- const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc));
- const currentWindow = doc.defaultView;
- if (!currentWindow) {
- return () => {
- handlers.forEach((h) => h());
- };
- }
- const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value');
- const hookProperties = [
- [currentWindow.HTMLInputElement.prototype, 'value'],
- [currentWindow.HTMLInputElement.prototype, 'checked'],
- [currentWindow.HTMLSelectElement.prototype, 'value'],
- [currentWindow.HTMLTextAreaElement.prototype, 'value'],
- [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'],
- [currentWindow.HTMLOptionElement.prototype, 'selected'],
- ];
- if (propertyDescriptor && propertyDescriptor.set) {
- handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], {
- set() {
- callbackWrapper(eventHandler)({
- target: this,
- isTrusted: false,
- });
- },
- }, false, currentWindow)));
- }
- return callbackWrapper(() => {
- handlers.forEach((h) => h());
- });
- }
- function getNestedCSSRulePositions(rule) {
- const positions = [];
- function recurse(childRule, pos) {
- if ((hasNestedCSSRule('CSSGroupingRule') &&
- childRule.parentRule instanceof CSSGroupingRule) ||
- (hasNestedCSSRule('CSSMediaRule') &&
- childRule.parentRule instanceof CSSMediaRule) ||
- (hasNestedCSSRule('CSSSupportsRule') &&
- childRule.parentRule instanceof CSSSupportsRule) ||
- (hasNestedCSSRule('CSSConditionRule') &&
- childRule.parentRule instanceof CSSConditionRule)) {
- const rules = Array.from(childRule.parentRule.cssRules);
- const index = rules.indexOf(childRule);
- pos.unshift(index);
- }
- else if (childRule.parentStyleSheet) {
- const rules = Array.from(childRule.parentStyleSheet.cssRules);
- const index = rules.indexOf(childRule);
- pos.unshift(index);
- }
- return pos;
- }
- return recurse(rule, positions);
- }
- function getIdAndStyleId(sheet, mirror, styleMirror) {
- let id, styleId;
- if (!sheet)
- return {};
- if (sheet.ownerNode)
- id = mirror.getId(sheet.ownerNode);
- else
- styleId = styleMirror.getId(sheet);
- return {
- styleId,
- id,
- };
- }
- function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) {
- if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) {
- return () => {
- };
- }
- const insertRule = win.CSSStyleSheet.prototype.insertRule;
- win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [rule, index] = argumentsList;
- const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleSheetRuleCb({
- id,
- styleId,
- adds: [{ rule, index }],
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- const deleteRule = win.CSSStyleSheet.prototype.deleteRule;
- win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [index] = argumentsList;
- const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleSheetRuleCb({
- id,
- styleId,
- removes: [{ index }],
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- let replace;
- if (win.CSSStyleSheet.prototype.replace) {
- replace = win.CSSStyleSheet.prototype.replace;
- win.CSSStyleSheet.prototype.replace = new Proxy(replace, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [text] = argumentsList;
- const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleSheetRuleCb({
- id,
- styleId,
- replace: text,
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- }
- let replaceSync;
- if (win.CSSStyleSheet.prototype.replaceSync) {
- replaceSync = win.CSSStyleSheet.prototype.replaceSync;
- win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [text] = argumentsList;
- const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleSheetRuleCb({
- id,
- styleId,
- replaceSync: text,
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- }
- const supportedNestedCSSRuleTypes = {};
- if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) {
- supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule;
- }
- else {
- if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) {
- supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule;
- }
- if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) {
- supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule;
- }
- if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) {
- supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule;
- }
- }
- const unmodifiedFunctions = {};
- Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
- unmodifiedFunctions[typeKey] = {
- insertRule: type.prototype.insertRule,
- deleteRule: type.prototype.deleteRule,
- };
- type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [rule, index] = argumentsList;
- const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleSheetRuleCb({
- id,
- styleId,
- adds: [
- {
- rule,
- index: [
- ...getNestedCSSRulePositions(thisArg),
- index || 0,
- ],
- },
- ],
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [index] = argumentsList;
- const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleSheetRuleCb({
- id,
- styleId,
- removes: [
- { index: [...getNestedCSSRulePositions(thisArg), index] },
- ],
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- });
- return callbackWrapper(() => {
- win.CSSStyleSheet.prototype.insertRule = insertRule;
- win.CSSStyleSheet.prototype.deleteRule = deleteRule;
- replace && (win.CSSStyleSheet.prototype.replace = replace);
- replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync);
- Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
- type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
- type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
- });
- });
- }
- function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) {
- let hostId = null;
- if (host.nodeName === '#document')
- hostId = mirror.getId(host);
- else
- hostId = mirror.getId(host.host);
- const patchTarget = host.nodeName === '#document'
- ? _optionalChain$2([host, 'access', _4 => _4.defaultView, 'optionalAccess', _5 => _5.Document])
- : _optionalChain$2([host, 'access', _6 => _6.ownerDocument, 'optionalAccess', _7 => _7.defaultView, 'optionalAccess', _8 => _8.ShadowRoot]);
- const originalPropertyDescriptor = _optionalChain$2([patchTarget, 'optionalAccess', _9 => _9.prototype])
- ? Object.getOwnPropertyDescriptor(_optionalChain$2([patchTarget, 'optionalAccess', _10 => _10.prototype]), 'adoptedStyleSheets')
- : undefined;
- if (hostId === null ||
- hostId === -1 ||
- !patchTarget ||
- !originalPropertyDescriptor)
- return () => {
- };
- Object.defineProperty(host, 'adoptedStyleSheets', {
- configurable: originalPropertyDescriptor.configurable,
- enumerable: originalPropertyDescriptor.enumerable,
- get() {
- return _optionalChain$2([originalPropertyDescriptor, 'access', _11 => _11.get, 'optionalAccess', _12 => _12.call, 'call', _13 => _13(this)]);
- },
- set(sheets) {
- const result = _optionalChain$2([originalPropertyDescriptor, 'access', _14 => _14.set, 'optionalAccess', _15 => _15.call, 'call', _16 => _16(this, sheets)]);
- if (hostId !== null && hostId !== -1) {
- try {
- stylesheetManager.adoptStyleSheets(sheets, hostId);
- }
- catch (e) {
- }
- }
- return result;
- },
- });
- return callbackWrapper(() => {
- Object.defineProperty(host, 'adoptedStyleSheets', {
- configurable: originalPropertyDescriptor.configurable,
- enumerable: originalPropertyDescriptor.enumerable,
- get: originalPropertyDescriptor.get,
- set: originalPropertyDescriptor.set,
- });
- });
- }
- function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) {
- const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
- win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [property, value, priority] = argumentsList;
- if (ignoreCSSAttributes.has(property)) {
- return setProperty.apply(thisArg, [property, value, priority]);
- }
- const { id, styleId } = getIdAndStyleId(_optionalChain$2([thisArg, 'access', _17 => _17.parentRule, 'optionalAccess', _18 => _18.parentStyleSheet]), mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleDeclarationCb({
- id,
- styleId,
- set: {
- property,
- value,
- priority,
- },
- index: getNestedCSSRulePositions(thisArg.parentRule),
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty;
- win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, {
- apply: callbackWrapper((target, thisArg, argumentsList) => {
- const [property] = argumentsList;
- if (ignoreCSSAttributes.has(property)) {
- return removeProperty.apply(thisArg, [property]);
- }
- const { id, styleId } = getIdAndStyleId(_optionalChain$2([thisArg, 'access', _19 => _19.parentRule, 'optionalAccess', _20 => _20.parentStyleSheet]), mirror, stylesheetManager.styleMirror);
- if ((id && id !== -1) || (styleId && styleId !== -1)) {
- styleDeclarationCb({
- id,
- styleId,
- remove: {
- property,
- },
- index: getNestedCSSRulePositions(thisArg.parentRule),
- });
- }
- return target.apply(thisArg, argumentsList);
- }),
- });
- return callbackWrapper(() => {
- win.CSSStyleDeclaration.prototype.setProperty = setProperty;
- win.CSSStyleDeclaration.prototype.removeProperty = removeProperty;
- });
- }
- function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, unblockSelector, mirror, sampling, doc, }) {
- const handler = callbackWrapper((type) => throttle$1(callbackWrapper((event) => {
- const target = getEventTarget(event);
- if (!target ||
- isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
- return;
- }
- const { currentTime, volume, muted, playbackRate } = target;
- mediaInteractionCb({
- type,
- id: mirror.getId(target),
- currentTime,
- volume,
- muted,
- playbackRate,
- });
- }), sampling.media || 500));
- const handlers = [
- on('play', handler(0), doc),
- on('pause', handler(1), doc),
- on('seeked', handler(2), doc),
- on('volumechange', handler(3), doc),
- on('ratechange', handler(4), doc),
- ];
- return callbackWrapper(() => {
- handlers.forEach((h) => h());
- });
- }
- function initFontObserver({ fontCb, doc }) {
- const win = doc.defaultView;
- if (!win) {
- return () => {
- };
- }
- const handlers = [];
- const fontMap = new WeakMap();
- const originalFontFace = win.FontFace;
- win.FontFace = function FontFace(family, source, descriptors) {
- const fontFace = new originalFontFace(family, source, descriptors);
- fontMap.set(fontFace, {
- family,
- buffer: typeof source !== 'string',
- descriptors,
- fontSource: typeof source === 'string'
- ? source
- : JSON.stringify(Array.from(new Uint8Array(source))),
- });
- return fontFace;
- };
- const restoreHandler = patch(doc.fonts, 'add', function (original) {
- return function (fontFace) {
- setTimeout(callbackWrapper(() => {
- const p = fontMap.get(fontFace);
- if (p) {
- fontCb(p);
- fontMap.delete(fontFace);
- }
- }), 0);
- return original.apply(this, [fontFace]);
- };
- });
- handlers.push(() => {
- win.FontFace = originalFontFace;
- });
- handlers.push(restoreHandler);
- return callbackWrapper(() => {
- handlers.forEach((h) => h());
- });
- }
- function initSelectionObserver(param) {
- const { doc, mirror, blockClass, blockSelector, unblockSelector, selectionCb, } = param;
- let collapsed = true;
- const updateSelection = callbackWrapper(() => {
- const selection = doc.getSelection();
- if (!selection || (collapsed && _optionalChain$2([selection, 'optionalAccess', _21 => _21.isCollapsed])))
- return;
- collapsed = selection.isCollapsed || false;
- const ranges = [];
- const count = selection.rangeCount || 0;
- for (let i = 0; i < count; i++) {
- const range = selection.getRangeAt(i);
- const { startContainer, startOffset, endContainer, endOffset } = range;
- const blocked = isBlocked(startContainer, blockClass, blockSelector, unblockSelector, true) ||
- isBlocked(endContainer, blockClass, blockSelector, unblockSelector, true);
- if (blocked)
- continue;
- ranges.push({
- start: mirror.getId(startContainer),
- startOffset,
- end: mirror.getId(endContainer),
- endOffset,
- });
- }
- selectionCb({ ranges });
- });
- updateSelection();
- return on('selectionchange', updateSelection);
- }
- function initCustomElementObserver({ doc, customElementCb, }) {
- const win = doc.defaultView;
- if (!win || !win.customElements)
- return () => { };
- const restoreHandler = patch(win.customElements, 'define', function (original) {
- return function (name, constructor, options) {
- try {
- customElementCb({
- define: {
- name,
- },
- });
- }
- catch (e) {
- }
- return original.apply(this, [name, constructor, options]);
- };
- });
- return restoreHandler;
- }
- function initObservers(o, _hooks = {}) {
- const currentWindow = o.doc.defaultView;
- if (!currentWindow) {
- return () => {
- };
- }
- const mutationObserver = initMutationObserver(o, o.doc);
- const mousemoveHandler = initMoveObserver(o);
- const mouseInteractionHandler = initMouseInteractionObserver(o);
- const scrollHandler = initScrollObserver(o);
- const viewportResizeHandler = initViewportResizeObserver(o, {
- win: currentWindow,
- });
- const inputHandler = initInputObserver(o);
- const mediaInteractionHandler = initMediaInteractionObserver(o);
- const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow });
- const adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc);
- const styleDeclarationObserver = initStyleDeclarationObserver(o, {
- win: currentWindow,
- });
- const fontObserver = o.collectFonts
- ? initFontObserver(o)
- : () => {
- };
- const selectionObserver = initSelectionObserver(o);
- const customElementObserver = initCustomElementObserver(o);
- const pluginHandlers = [];
- for (const plugin of o.plugins) {
- pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options));
- }
- return callbackWrapper(() => {
- mutationBuffers.forEach((b) => b.reset());
- mutationObserver.disconnect();
- mousemoveHandler();
- mouseInteractionHandler();
- scrollHandler();
- viewportResizeHandler();
- inputHandler();
- mediaInteractionHandler();
- styleSheetObserver();
- adoptedStyleSheetObserver();
- styleDeclarationObserver();
- fontObserver();
- selectionObserver();
- customElementObserver();
- pluginHandlers.forEach((h) => h());
- });
- }
- function hasNestedCSSRule(prop) {
- return typeof window[prop] !== 'undefined';
- }
- function canMonkeyPatchNestedCSSRule(prop) {
- return Boolean(typeof window[prop] !== 'undefined' &&
- window[prop].prototype &&
- 'insertRule' in window[prop].prototype &&
- 'deleteRule' in window[prop].prototype);
- }
- class CrossOriginIframeMirror {
- constructor(generateIdFn) {
- this.generateIdFn = generateIdFn;
- this.iframeIdToRemoteIdMap = new WeakMap();
- this.iframeRemoteIdToIdMap = new WeakMap();
- }
- getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) {
- const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe);
- const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe);
- let id = idToRemoteIdMap.get(remoteId);
- if (!id) {
- id = this.generateIdFn();
- idToRemoteIdMap.set(remoteId, id);
- remoteIdToIdMap.set(id, remoteId);
- }
- return id;
- }
- getIds(iframe, remoteId) {
- const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe);
- const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
- return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap));
- }
- getRemoteId(iframe, id, map) {
- const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe);
- if (typeof id !== 'number')
- return id;
- const remoteId = remoteIdToIdMap.get(id);
- if (!remoteId)
- return -1;
- return remoteId;
- }
- getRemoteIds(iframe, ids) {
- const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
- return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap));
- }
- reset(iframe) {
- if (!iframe) {
- this.iframeIdToRemoteIdMap = new WeakMap();
- this.iframeRemoteIdToIdMap = new WeakMap();
- return;
- }
- this.iframeIdToRemoteIdMap.delete(iframe);
- this.iframeRemoteIdToIdMap.delete(iframe);
- }
- getIdToRemoteIdMap(iframe) {
- let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe);
- if (!idToRemoteIdMap) {
- idToRemoteIdMap = new Map();
- this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap);
- }
- return idToRemoteIdMap;
- }
- getRemoteIdToIdMap(iframe) {
- let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe);
- if (!remoteIdToIdMap) {
- remoteIdToIdMap = new Map();
- this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap);
- }
- return remoteIdToIdMap;
- }
- }
- function _optionalChain$1(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
- class IframeManagerNoop {
- constructor() {
- this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
- this.crossOriginIframeRootIdMap = new WeakMap();
- }
- addIframe() {
- }
- addLoadListener() {
- }
- attachIframe() {
- }
- }
- class IframeManager {
- constructor(options) {
- this.iframes = new WeakMap();
- this.crossOriginIframeMap = new WeakMap();
- this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
- this.crossOriginIframeRootIdMap = new WeakMap();
- this.mutationCb = options.mutationCb;
- this.wrappedEmit = options.wrappedEmit;
- this.stylesheetManager = options.stylesheetManager;
- this.recordCrossOriginIframes = options.recordCrossOriginIframes;
- this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror));
- this.mirror = options.mirror;
- if (this.recordCrossOriginIframes) {
- window.addEventListener('message', this.handleMessage.bind(this));
- }
- }
- addIframe(iframeEl) {
- this.iframes.set(iframeEl, true);
- if (iframeEl.contentWindow)
- this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
- }
- addLoadListener(cb) {
- this.loadListener = cb;
- }
- attachIframe(iframeEl, childSn) {
- this.mutationCb({
- adds: [
- {
- parentId: this.mirror.getId(iframeEl),
- nextId: null,
- node: childSn,
- },
- ],
- removes: [],
- texts: [],
- attributes: [],
- isAttachIframe: true,
- });
- _optionalChain$1([this, 'access', _ => _.loadListener, 'optionalCall', _2 => _2(iframeEl)]);
- if (iframeEl.contentDocument &&
- iframeEl.contentDocument.adoptedStyleSheets &&
- iframeEl.contentDocument.adoptedStyleSheets.length > 0)
- this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
- }
- handleMessage(message) {
- const crossOriginMessageEvent = message;
- if (crossOriginMessageEvent.data.type !== 'rrweb' ||
- crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin)
- return;
- const iframeSourceWindow = message.source;
- if (!iframeSourceWindow)
- return;
- const iframeEl = this.crossOriginIframeMap.get(message.source);
- if (!iframeEl)
- return;
- const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event);
- if (transformedEvent)
- this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout);
- }
- transformCrossOriginEvent(iframeEl, e) {
- switch (e.type) {
- case EventType.FullSnapshot: {
- this.crossOriginIframeMirror.reset(iframeEl);
- this.crossOriginIframeStyleMirror.reset(iframeEl);
- this.replaceIdOnNode(e.data.node, iframeEl);
- const rootId = e.data.node.id;
- this.crossOriginIframeRootIdMap.set(iframeEl, rootId);
- this.patchRootIdOnNode(e.data.node, rootId);
- return {
- timestamp: e.timestamp,
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.Mutation,
- adds: [
- {
- parentId: this.mirror.getId(iframeEl),
- nextId: null,
- node: e.data.node,
- },
- ],
- removes: [],
- texts: [],
- attributes: [],
- isAttachIframe: true,
- },
- };
- }
- case EventType.Meta:
- case EventType.Load:
- case EventType.DomContentLoaded: {
- return false;
- }
- case EventType.Plugin: {
- return e;
- }
- case EventType.Custom: {
- this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']);
- return e;
- }
- case EventType.IncrementalSnapshot: {
- switch (e.data.source) {
- case IncrementalSource.Mutation: {
- e.data.adds.forEach((n) => {
- this.replaceIds(n, iframeEl, [
- 'parentId',
- 'nextId',
- 'previousId',
- ]);
- this.replaceIdOnNode(n.node, iframeEl);
- const rootId = this.crossOriginIframeRootIdMap.get(iframeEl);
- rootId && this.patchRootIdOnNode(n.node, rootId);
- });
- e.data.removes.forEach((n) => {
- this.replaceIds(n, iframeEl, ['parentId', 'id']);
- });
- e.data.attributes.forEach((n) => {
- this.replaceIds(n, iframeEl, ['id']);
- });
- e.data.texts.forEach((n) => {
- this.replaceIds(n, iframeEl, ['id']);
- });
- return e;
- }
- case IncrementalSource.Drag:
- case IncrementalSource.TouchMove:
- case IncrementalSource.MouseMove: {
- e.data.positions.forEach((p) => {
- this.replaceIds(p, iframeEl, ['id']);
- });
- return e;
- }
- case IncrementalSource.ViewportResize: {
- return false;
- }
- case IncrementalSource.MediaInteraction:
- case IncrementalSource.MouseInteraction:
- case IncrementalSource.Scroll:
- case IncrementalSource.CanvasMutation:
- case IncrementalSource.Input: {
- this.replaceIds(e.data, iframeEl, ['id']);
- return e;
- }
- case IncrementalSource.StyleSheetRule:
- case IncrementalSource.StyleDeclaration: {
- this.replaceIds(e.data, iframeEl, ['id']);
- this.replaceStyleIds(e.data, iframeEl, ['styleId']);
- return e;
- }
- case IncrementalSource.Font: {
- return e;
- }
- case IncrementalSource.Selection: {
- e.data.ranges.forEach((range) => {
- this.replaceIds(range, iframeEl, ['start', 'end']);
- });
- return e;
- }
- case IncrementalSource.AdoptedStyleSheet: {
- this.replaceIds(e.data, iframeEl, ['id']);
- this.replaceStyleIds(e.data, iframeEl, ['styleIds']);
- _optionalChain$1([e, 'access', _3 => _3.data, 'access', _4 => _4.styles, 'optionalAccess', _5 => _5.forEach, 'call', _6 => _6((style) => {
- this.replaceStyleIds(style, iframeEl, ['styleId']);
- })]);
- return e;
- }
- }
- }
- }
- return false;
- }
- replace(iframeMirror, obj, iframeEl, keys) {
- for (const key of keys) {
- if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number')
- continue;
- if (Array.isArray(obj[key])) {
- obj[key] = iframeMirror.getIds(iframeEl, obj[key]);
- }
- else {
- obj[key] = iframeMirror.getId(iframeEl, obj[key]);
- }
- }
- return obj;
- }
- replaceIds(obj, iframeEl, keys) {
- return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys);
- }
- replaceStyleIds(obj, iframeEl, keys) {
- return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys);
- }
- replaceIdOnNode(node, iframeEl) {
- this.replaceIds(node, iframeEl, ['id', 'rootId']);
- if ('childNodes' in node) {
- node.childNodes.forEach((child) => {
- this.replaceIdOnNode(child, iframeEl);
- });
- }
- }
- patchRootIdOnNode(node, rootId) {
- if (node.type !== NodeType$1.Document && !node.rootId)
- node.rootId = rootId;
- if ('childNodes' in node) {
- node.childNodes.forEach((child) => {
- this.patchRootIdOnNode(child, rootId);
- });
- }
- }
- }
- class ShadowDomManagerNoop {
- init() {
- }
- addShadowRoot() {
- }
- observeAttachShadow() {
- }
- reset() {
- }
- }
- class ShadowDomManager {
- constructor(options) {
- this.shadowDoms = new WeakSet();
- this.restoreHandlers = [];
- this.mutationCb = options.mutationCb;
- this.scrollCb = options.scrollCb;
- this.bypassOptions = options.bypassOptions;
- this.mirror = options.mirror;
- this.init();
- }
- init() {
- this.reset();
- this.patchAttachShadow(Element, document);
- }
- addShadowRoot(shadowRoot, doc) {
- if (!isNativeShadowDom(shadowRoot))
- return;
- if (this.shadowDoms.has(shadowRoot))
- return;
- this.shadowDoms.add(shadowRoot);
- const observer = initMutationObserver({
- ...this.bypassOptions,
- doc,
- mutationCb: this.mutationCb,
- mirror: this.mirror,
- shadowDomManager: this,
- }, shadowRoot);
- this.restoreHandlers.push(() => observer.disconnect());
- this.restoreHandlers.push(initScrollObserver({
- ...this.bypassOptions,
- scrollCb: this.scrollCb,
- doc: shadowRoot,
- mirror: this.mirror,
- }));
- setTimeout(() => {
- if (shadowRoot.adoptedStyleSheets &&
- shadowRoot.adoptedStyleSheets.length > 0)
- this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host));
- this.restoreHandlers.push(initAdoptedStyleSheetObserver({
- mirror: this.mirror,
- stylesheetManager: this.bypassOptions.stylesheetManager,
- }, shadowRoot));
- }, 0);
- }
- observeAttachShadow(iframeElement) {
- if (!iframeElement.contentWindow || !iframeElement.contentDocument)
- return;
- this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument);
- }
- patchAttachShadow(element, doc) {
- const manager = this;
- this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) {
- return function (option) {
- const shadowRoot = original.call(this, option);
- if (this.shadowRoot && inDom(this))
- manager.addShadowRoot(this.shadowRoot, doc);
- return shadowRoot;
- };
- }));
- }
- reset() {
- this.restoreHandlers.forEach((handler) => {
- try {
- handler();
- }
- catch (e) {
- }
- });
- this.restoreHandlers = [];
- this.shadowDoms = new WeakSet();
- }
- }
- class CanvasManagerNoop {
- reset() {
- }
- freeze() {
- }
- unfreeze() {
- }
- lock() {
- }
- unlock() {
- }
- snapshot() {
- }
- }
- class StylesheetManager {
- constructor(options) {
- this.trackedLinkElements = new WeakSet();
- this.styleMirror = new StyleSheetMirror();
- this.mutationCb = options.mutationCb;
- this.adoptedStyleSheetCb = options.adoptedStyleSheetCb;
- }
- attachLinkElement(linkEl, childSn) {
- if ('_cssText' in childSn.attributes)
- this.mutationCb({
- adds: [],
- removes: [],
- texts: [],
- attributes: [
- {
- id: childSn.id,
- attributes: childSn
- .attributes,
- },
- ],
- });
- this.trackLinkElement(linkEl);
- }
- trackLinkElement(linkEl) {
- if (this.trackedLinkElements.has(linkEl))
- return;
- this.trackedLinkElements.add(linkEl);
- this.trackStylesheetInLinkElement(linkEl);
- }
- adoptStyleSheets(sheets, hostId) {
- if (sheets.length === 0)
- return;
- const adoptedStyleSheetData = {
- id: hostId,
- styleIds: [],
- };
- const styles = [];
- for (const sheet of sheets) {
- let styleId;
- if (!this.styleMirror.has(sheet)) {
- styleId = this.styleMirror.add(sheet);
- styles.push({
- styleId,
- rules: Array.from(sheet.rules || CSSRule, (r, index) => ({
- rule: stringifyRule(r),
- index,
- })),
- });
- }
- else
- styleId = this.styleMirror.getId(sheet);
- adoptedStyleSheetData.styleIds.push(styleId);
- }
- if (styles.length > 0)
- adoptedStyleSheetData.styles = styles;
- this.adoptedStyleSheetCb(adoptedStyleSheetData);
- }
- reset() {
- this.styleMirror.reset();
- this.trackedLinkElements = new WeakSet();
- }
- trackStylesheetInLinkElement(linkEl) {
- }
- }
- class ProcessedNodeManager {
- constructor() {
- this.nodeMap = new WeakMap();
- this.loop = true;
- this.periodicallyClear();
- }
- periodicallyClear() {
- onRequestAnimationFrame(() => {
- this.clear();
- if (this.loop)
- this.periodicallyClear();
- });
- }
- inOtherBuffer(node, thisBuffer) {
- const buffers = this.nodeMap.get(node);
- return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer));
- }
- add(node, buffer) {
- this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer));
- }
- clear() {
- this.nodeMap = new WeakMap();
- }
- destroy() {
- this.loop = false;
- }
- }
- function wrapEvent(e) {
- const eWithTime = e;
- eWithTime.timestamp = nowTimestamp();
- return eWithTime;
- }
- let _takeFullSnapshot;
- const mirror = createMirror();
- function record(options = {}) {
- const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, unblockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskAllText = false, maskTextClass = 'rr-mask', unmaskTextClass = null, maskTextSelector = null, unmaskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskAttributeFn, maskInputFn, maskTextFn, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded'
- ? options.recordAfter
- : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, onMutation, getCanvasManager, } = options;
- registerErrorHandler(errorHandler);
- const inEmittingFrame = recordCrossOriginIframes
- ? window.parent === window
- : true;
- let passEmitsToParent = false;
- if (!inEmittingFrame) {
- try {
- if (window.parent.document) {
- passEmitsToParent = false;
- }
- }
- catch (e) {
- passEmitsToParent = true;
- }
- }
- if (inEmittingFrame && !emit) {
- throw new Error('emit function is required');
- }
- if (mousemoveWait !== undefined && sampling.mousemove === undefined) {
- sampling.mousemove = mousemoveWait;
- }
- mirror.reset();
- const maskInputOptions = maskAllInputs === true
- ? {
- color: true,
- date: true,
- 'datetime-local': true,
- email: true,
- month: true,
- number: true,
- range: true,
- search: true,
- tel: true,
- text: true,
- time: true,
- url: true,
- week: true,
- textarea: true,
- select: true,
- radio: true,
- checkbox: true,
- }
- : _maskInputOptions !== undefined
- ? _maskInputOptions
- : {};
- const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all'
- ? {
- script: true,
- comment: true,
- headFavicon: true,
- headWhitespace: true,
- headMetaSocial: true,
- headMetaRobots: true,
- headMetaHttpEquiv: true,
- headMetaVerification: true,
- headMetaAuthorship: _slimDOMOptions === 'all',
- headMetaDescKeywords: _slimDOMOptions === 'all',
- }
- : _slimDOMOptions
- ? _slimDOMOptions
- : {};
- polyfill();
- let lastFullSnapshotEvent;
- let incrementalSnapshotCount = 0;
- const eventProcessor = (e) => {
- for (const plugin of plugins || []) {
- if (plugin.eventProcessor) {
- e = plugin.eventProcessor(e);
- }
- }
- if (packFn &&
- !passEmitsToParent) {
- e = packFn(e);
- }
- return e;
- };
- const wrappedEmit = (e, isCheckout) => {
- if (_optionalChain([mutationBuffers, 'access', _ => _[0], 'optionalAccess', _2 => _2.isFrozen, 'call', _3 => _3()]) &&
- e.type !== EventType.FullSnapshot &&
- !(e.type === EventType.IncrementalSnapshot &&
- e.data.source === IncrementalSource.Mutation)) {
- mutationBuffers.forEach((buf) => buf.unfreeze());
- }
- if (inEmittingFrame) {
- _optionalChain([emit, 'optionalCall', _4 => _4(eventProcessor(e), isCheckout)]);
- }
- else if (passEmitsToParent) {
- const message = {
- type: 'rrweb',
- event: eventProcessor(e),
- origin: window.location.origin,
- isCheckout,
- };
- window.parent.postMessage(message, '*');
- }
- if (e.type === EventType.FullSnapshot) {
- lastFullSnapshotEvent = e;
- incrementalSnapshotCount = 0;
- }
- else if (e.type === EventType.IncrementalSnapshot) {
- if (e.data.source === IncrementalSource.Mutation &&
- e.data.isAttachIframe) {
- return;
- }
- incrementalSnapshotCount++;
- const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth;
- const exceedTime = checkoutEveryNms &&
- e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms;
- if (exceedCount || exceedTime) {
- takeFullSnapshot(true);
- }
- }
- };
- const wrappedMutationEmit = (m) => {
- wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.Mutation,
- ...m,
- },
- }));
- };
- const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.Scroll,
- ...p,
- },
- }));
- const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.CanvasMutation,
- ...p,
- },
- }));
- const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.AdoptedStyleSheet,
- ...a,
- },
- }));
- const stylesheetManager = new StylesheetManager({
- mutationCb: wrappedMutationEmit,
- adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit,
- });
- const iframeManager = typeof __RRWEB_EXCLUDE_IFRAME__ === 'boolean' && __RRWEB_EXCLUDE_IFRAME__
- ? new IframeManagerNoop()
- : new IframeManager({
- mirror,
- mutationCb: wrappedMutationEmit,
- stylesheetManager: stylesheetManager,
- recordCrossOriginIframes,
- wrappedEmit,
- });
- for (const plugin of plugins || []) {
- if (plugin.getMirror)
- plugin.getMirror({
- nodeMirror: mirror,
- crossOriginIframeMirror: iframeManager.crossOriginIframeMirror,
- crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror,
- });
- }
- const processedNodeManager = new ProcessedNodeManager();
- const canvasManager = _getCanvasManager(getCanvasManager, {
- mirror,
- win: window,
- mutationCb: (p) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.CanvasMutation,
- ...p,
- },
- })),
- recordCanvas,
- blockClass,
- blockSelector,
- unblockSelector,
- sampling: sampling['canvas'],
- dataURLOptions,
- errorHandler,
- });
- const shadowDomManager = typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' &&
- __RRWEB_EXCLUDE_SHADOW_DOM__
- ? new ShadowDomManagerNoop()
- : new ShadowDomManager({
- mutationCb: wrappedMutationEmit,
- scrollCb: wrappedScrollEmit,
- bypassOptions: {
- onMutation,
- blockClass,
- blockSelector,
- unblockSelector,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- inlineStylesheet,
- maskInputOptions,
- dataURLOptions,
- maskAttributeFn,
- maskTextFn,
- maskInputFn,
- recordCanvas,
- inlineImages,
- sampling,
- slimDOMOptions,
- iframeManager,
- stylesheetManager,
- canvasManager,
- keepIframeSrcFn,
- processedNodeManager,
- },
- mirror,
- });
- const takeFullSnapshot = (isCheckout = false) => {
- wrappedEmit(wrapEvent({
- type: EventType.Meta,
- data: {
- href: window.location.href,
- width: getWindowWidth(),
- height: getWindowHeight(),
- },
- }), isCheckout);
- stylesheetManager.reset();
- shadowDomManager.init();
- mutationBuffers.forEach((buf) => buf.lock());
- const node = snapshot(document, {
- mirror,
- blockClass,
- blockSelector,
- unblockSelector,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- inlineStylesheet,
- maskAllInputs: maskInputOptions,
- maskAttributeFn,
- maskInputFn,
- maskTextFn,
- slimDOM: slimDOMOptions,
- dataURLOptions,
- recordCanvas,
- inlineImages,
- onSerialize: (n) => {
- if (isSerializedIframe(n, mirror)) {
- iframeManager.addIframe(n);
- }
- if (isSerializedStylesheet(n, mirror)) {
- stylesheetManager.trackLinkElement(n);
- }
- if (hasShadowRoot(n)) {
- shadowDomManager.addShadowRoot(n.shadowRoot, document);
- }
- },
- onIframeLoad: (iframe, childSn) => {
- iframeManager.attachIframe(iframe, childSn);
- shadowDomManager.observeAttachShadow(iframe);
- },
- onStylesheetLoad: (linkEl, childSn) => {
- stylesheetManager.attachLinkElement(linkEl, childSn);
- },
- keepIframeSrcFn,
- });
- if (!node) {
- return console.warn('Failed to snapshot the document');
- }
- wrappedEmit(wrapEvent({
- type: EventType.FullSnapshot,
- data: {
- node,
- initialOffset: getWindowScroll(window),
- },
- }));
- mutationBuffers.forEach((buf) => buf.unlock());
- if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0)
- stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document));
- };
- _takeFullSnapshot = takeFullSnapshot;
- try {
- const handlers = [];
- const observe = (doc) => {
- return callbackWrapper(initObservers)({
- onMutation,
- mutationCb: wrappedMutationEmit,
- mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source,
- positions,
- },
- })),
- mouseInteractionCb: (d) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.MouseInteraction,
- ...d,
- },
- })),
- scrollCb: wrappedScrollEmit,
- viewportResizeCb: (d) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.ViewportResize,
- ...d,
- },
- })),
- inputCb: (v) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.Input,
- ...v,
- },
- })),
- mediaInteractionCb: (p) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.MediaInteraction,
- ...p,
- },
- })),
- styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.StyleSheetRule,
- ...r,
- },
- })),
- styleDeclarationCb: (r) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.StyleDeclaration,
- ...r,
- },
- })),
- canvasMutationCb: wrappedCanvasMutationEmit,
- fontCb: (p) => wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.Font,
- ...p,
- },
- })),
- selectionCb: (p) => {
- wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.Selection,
- ...p,
- },
- }));
- },
- customElementCb: (c) => {
- wrappedEmit(wrapEvent({
- type: EventType.IncrementalSnapshot,
- data: {
- source: IncrementalSource.CustomElement,
- ...c,
- },
- }));
- },
- blockClass,
- ignoreClass,
- ignoreSelector,
- maskAllText,
- maskTextClass,
- unmaskTextClass,
- maskTextSelector,
- unmaskTextSelector,
- maskInputOptions,
- inlineStylesheet,
- sampling,
- recordCanvas,
- inlineImages,
- userTriggeredOnInput,
- collectFonts,
- doc,
- maskAttributeFn,
- maskInputFn,
- maskTextFn,
- keepIframeSrcFn,
- blockSelector,
- unblockSelector,
- slimDOMOptions,
- dataURLOptions,
- mirror,
- iframeManager,
- stylesheetManager,
- shadowDomManager,
- processedNodeManager,
- canvasManager,
- ignoreCSSAttributes,
- plugins: _optionalChain([plugins
- , 'optionalAccess', _5 => _5.filter, 'call', _6 => _6((p) => p.observer)
- , 'optionalAccess', _7 => _7.map, 'call', _8 => _8((p) => ({
- observer: p.observer,
- options: p.options,
- callback: (payload) => wrappedEmit(wrapEvent({
- type: EventType.Plugin,
- data: {
- plugin: p.name,
- payload,
- },
- })),
- }))]) || [],
- }, {});
- };
- iframeManager.addLoadListener((iframeEl) => {
- try {
- handlers.push(observe(iframeEl.contentDocument));
- }
- catch (error) {
- console.warn(error);
- }
- });
- const init = () => {
- takeFullSnapshot();
- handlers.push(observe(document));
- };
- if (document.readyState === 'interactive' ||
- document.readyState === 'complete') {
- init();
- }
- else {
- handlers.push(on('DOMContentLoaded', () => {
- wrappedEmit(wrapEvent({
- type: EventType.DomContentLoaded,
- data: {},
- }));
- if (recordAfter === 'DOMContentLoaded')
- init();
- }));
- handlers.push(on('load', () => {
- wrappedEmit(wrapEvent({
- type: EventType.Load,
- data: {},
- }));
- if (recordAfter === 'load')
- init();
- }, window));
- }
- return () => {
- handlers.forEach((h) => h());
- processedNodeManager.destroy();
- _takeFullSnapshot = undefined;
- unregisterErrorHandler();
- };
- }
- catch (error) {
- console.warn(error);
- }
- }
- function takeFullSnapshot(isCheckout) {
- if (!_takeFullSnapshot) {
- throw new Error('please take full snapshot after start recording');
- }
- _takeFullSnapshot(isCheckout);
- }
- record.mirror = mirror;
- record.takeFullSnapshot = takeFullSnapshot;
- function _getCanvasManager(getCanvasManagerFn, options) {
- try {
- return getCanvasManagerFn
- ? getCanvasManagerFn(options)
- : new CanvasManagerNoop();
- }
- catch (e2) {
- console.warn('Unable to initialize CanvasManager');
- return new CanvasManagerNoop();
- }
- }
- const ReplayEventTypeIncrementalSnapshot = 3;
- const ReplayEventTypeCustom = 5;
- /**
- * Converts a timestamp to ms, if it was in s, or keeps it as ms.
- */
- function timestampToMs(timestamp) {
- const isMs = timestamp > 9999999999;
- return isMs ? timestamp : timestamp * 1000;
- }
- /**
- * Converts a timestamp to s, if it was in ms, or keeps it as s.
- */
- function timestampToS(timestamp) {
- const isMs = timestamp > 9999999999;
- return isMs ? timestamp / 1000 : timestamp;
- }
- /**
- * Add a breadcrumb event to replay.
- */
- function addBreadcrumbEvent(replay, breadcrumb) {
- if (breadcrumb.category === 'sentry.transaction') {
- return;
- }
- if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
- replay.triggerUserActivity();
- } else {
- replay.checkAndHandleExpiredSession();
- }
- replay.addUpdate(() => {
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- replay.throttledAddEvent({
- type: EventType.Custom,
- // TODO: We were converting from ms to seconds for breadcrumbs, spans,
- // but maybe we should just keep them as milliseconds
- timestamp: (breadcrumb.timestamp || 0) * 1000,
- data: {
- tag: 'breadcrumb',
- // normalize to max. 10 depth and 1_000 properties per object
- payload: utils.normalize(breadcrumb, 10, 1000),
- },
- });
- // Do not flush after console log messages
- return breadcrumb.category === 'console';
- });
- }
- const INTERACTIVE_SELECTOR = 'button,a';
- /** Get the closest interactive parent element, or else return the given element. */
- function getClosestInteractive(element) {
- const closestInteractive = element.closest(INTERACTIVE_SELECTOR);
- return closestInteractive || element;
- }
- /**
- * For clicks, we check if the target is inside of a button or link
- * If so, we use this as the target instead
- * This is useful because if you click on the image in <button><img></button>,
- * The target will be the image, not the button, which we don't want here
- */
- function getClickTargetNode(event) {
- const target = getTargetNode(event);
- if (!target || !(target instanceof Element)) {
- return target;
- }
- return getClosestInteractive(target);
- }
- /** Get the event target node. */
- function getTargetNode(event) {
- if (isEventWithTarget(event)) {
- return event.target ;
- }
- return event;
- }
- function isEventWithTarget(event) {
- return typeof event === 'object' && !!event && 'target' in event;
- }
- let handlers;
- /**
- * Register a handler to be called when `window.open()` is called.
- * Returns a cleanup function.
- */
- function onWindowOpen(cb) {
- // Ensure to only register this once
- if (!handlers) {
- handlers = [];
- monkeyPatchWindowOpen();
- }
- handlers.push(cb);
- return () => {
- const pos = handlers ? handlers.indexOf(cb) : -1;
- if (pos > -1) {
- (handlers ).splice(pos, 1);
- }
- };
- }
- function monkeyPatchWindowOpen() {
- utils.fill(WINDOW, 'open', function (originalWindowOpen) {
- return function (...args) {
- if (handlers) {
- try {
- handlers.forEach(handler => handler());
- } catch (e) {
- // ignore errors in here
- }
- }
- return originalWindowOpen.apply(WINDOW, args);
- };
- });
- }
- /** Handle a click. */
- function handleClick(clickDetector, clickBreadcrumb, node) {
- clickDetector.handleClick(clickBreadcrumb, node);
- }
- /** A click detector class that can be used to detect slow or rage clicks on elements. */
- class ClickDetector {
- // protected for testing
- constructor(
- replay,
- slowClickConfig,
- // Just for easier testing
- _addBreadcrumbEvent = addBreadcrumbEvent,
- ) {
- this._lastMutation = 0;
- this._lastScroll = 0;
- this._clicks = [];
- // We want everything in s, but options are in ms
- this._timeout = slowClickConfig.timeout / 1000;
- this._threshold = slowClickConfig.threshold / 1000;
- this._scollTimeout = slowClickConfig.scrollTimeout / 1000;
- this._replay = replay;
- this._ignoreSelector = slowClickConfig.ignoreSelector;
- this._addBreadcrumbEvent = _addBreadcrumbEvent;
- }
- /** Register click detection handlers on mutation or scroll. */
- addListeners() {
- const cleanupWindowOpen = onWindowOpen(() => {
- // Treat window.open as mutation
- this._lastMutation = nowInSeconds();
- });
- this._teardown = () => {
- cleanupWindowOpen();
- this._clicks = [];
- this._lastMutation = 0;
- this._lastScroll = 0;
- };
- }
- /** Clean up listeners. */
- removeListeners() {
- if (this._teardown) {
- this._teardown();
- }
- if (this._checkClickTimeout) {
- clearTimeout(this._checkClickTimeout);
- }
- }
- /** @inheritDoc */
- handleClick(breadcrumb, node) {
- if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) {
- return;
- }
- const newClick = {
- timestamp: timestampToS(breadcrumb.timestamp),
- clickBreadcrumb: breadcrumb,
- // Set this to 0 so we know it originates from the click breadcrumb
- clickCount: 0,
- node,
- };
- // If there was a click in the last 1s on the same element, ignore it - only keep a single reference per second
- if (
- this._clicks.some(click => click.node === newClick.node && Math.abs(click.timestamp - newClick.timestamp) < 1)
- ) {
- return;
- }
- this._clicks.push(newClick);
- // If this is the first new click, set a timeout to check for multi clicks
- if (this._clicks.length === 1) {
- this._scheduleCheckClicks();
- }
- }
- /** @inheritDoc */
- registerMutation(timestamp = Date.now()) {
- this._lastMutation = timestampToS(timestamp);
- }
- /** @inheritDoc */
- registerScroll(timestamp = Date.now()) {
- this._lastScroll = timestampToS(timestamp);
- }
- /** @inheritDoc */
- registerClick(element) {
- const node = getClosestInteractive(element);
- this._handleMultiClick(node );
- }
- /** Count multiple clicks on elements. */
- _handleMultiClick(node) {
- this._getClicks(node).forEach(click => {
- click.clickCount++;
- });
- }
- /** Get all pending clicks for a given node. */
- _getClicks(node) {
- return this._clicks.filter(click => click.node === node);
- }
- /** Check the clicks that happened. */
- _checkClicks() {
- const timedOutClicks = [];
- const now = nowInSeconds();
- this._clicks.forEach(click => {
- if (!click.mutationAfter && this._lastMutation) {
- click.mutationAfter = click.timestamp <= this._lastMutation ? this._lastMutation - click.timestamp : undefined;
- }
- if (!click.scrollAfter && this._lastScroll) {
- click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined;
- }
- // All of these are in seconds!
- if (click.timestamp + this._timeout <= now) {
- timedOutClicks.push(click);
- }
- });
- // Remove "old" clicks
- for (const click of timedOutClicks) {
- const pos = this._clicks.indexOf(click);
- if (pos > -1) {
- this._generateBreadcrumbs(click);
- this._clicks.splice(pos, 1);
- }
- }
- // Trigger new check, unless no clicks left
- if (this._clicks.length) {
- this._scheduleCheckClicks();
- }
- }
- /** Generate matching breadcrumb(s) for the click. */
- _generateBreadcrumbs(click) {
- const replay = this._replay;
- const hadScroll = click.scrollAfter && click.scrollAfter <= this._scollTimeout;
- const hadMutation = click.mutationAfter && click.mutationAfter <= this._threshold;
- const isSlowClick = !hadScroll && !hadMutation;
- const { clickCount, clickBreadcrumb } = click;
- // Slow click
- if (isSlowClick) {
- // If `mutationAfter` is set, it means a mutation happened after the threshold, but before the timeout
- // If not, it means we just timed out without scroll & mutation
- const timeAfterClickMs = Math.min(click.mutationAfter || this._timeout, this._timeout) * 1000;
- const endReason = timeAfterClickMs < this._timeout * 1000 ? 'mutation' : 'timeout';
- const breadcrumb = {
- type: 'default',
- message: clickBreadcrumb.message,
- timestamp: clickBreadcrumb.timestamp,
- category: 'ui.slowClickDetected',
- data: {
- ...clickBreadcrumb.data,
- url: WINDOW.location.href,
- route: replay.getCurrentRoute(),
- timeAfterClickMs,
- endReason,
- // If clickCount === 0, it means multiClick was not correctly captured here
- // - we still want to send 1 in this case
- clickCount: clickCount || 1,
- },
- };
- this._addBreadcrumbEvent(replay, breadcrumb);
- return;
- }
- // Multi click
- if (clickCount > 1) {
- const breadcrumb = {
- type: 'default',
- message: clickBreadcrumb.message,
- timestamp: clickBreadcrumb.timestamp,
- category: 'ui.multiClick',
- data: {
- ...clickBreadcrumb.data,
- url: WINDOW.location.href,
- route: replay.getCurrentRoute(),
- clickCount,
- metric: true,
- },
- };
- this._addBreadcrumbEvent(replay, breadcrumb);
- }
- }
- /** Schedule to check current clicks. */
- _scheduleCheckClicks() {
- if (this._checkClickTimeout) {
- clearTimeout(this._checkClickTimeout);
- }
- this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000);
- }
- }
- const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT'];
- /** exported for tests only */
- function ignoreElement(node, ignoreSelector) {
- if (!SLOW_CLICK_TAGS.includes(node.tagName)) {
- return true;
- }
- // If <input> tag, we only want to consider input[type='submit'] & input[type='button']
- if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) {
- return true;
- }
- // If <a> tag, detect special variants that may not lead to an action
- // If target !== _self, we may open the link somewhere else, which would lead to no action
- // Also, when downloading a file, we may not leave the page, but still not trigger an action
- if (
- node.tagName === 'A' &&
- (node.hasAttribute('download') || (node.hasAttribute('target') && node.getAttribute('target') !== '_self'))
- ) {
- return true;
- }
- if (ignoreSelector && node.matches(ignoreSelector)) {
- return true;
- }
- return false;
- }
- function isClickBreadcrumb(breadcrumb) {
- return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp);
- }
- // This is good enough for us, and is easier to test/mock than `timestampInSeconds`
- function nowInSeconds() {
- return Date.now() / 1000;
- }
- /** Update the click detector based on a recording event of rrweb. */
- function updateClickDetectorForRecordingEvent(clickDetector, event) {
- try {
- // note: We only consider incremental snapshots here
- // This means that any full snapshot is ignored for mutation detection - the reason is that we simply cannot know if a mutation happened here.
- // E.g. think that we are buffering, an error happens and we take a full snapshot because we switched to session mode -
- // in this scenario, we would not know if a dead click happened because of the error, which is a key dead click scenario.
- // Instead, by ignoring full snapshots, we have the risk that we generate a false positive
- // (if a mutation _did_ happen but was "swallowed" by the full snapshot)
- // But this should be more unlikely as we'd generally capture the incremental snapshot right away
- if (!isIncrementalEvent(event)) {
- return;
- }
- const { source } = event.data;
- if (source === IncrementalSource.Mutation) {
- clickDetector.registerMutation(event.timestamp);
- }
- if (source === IncrementalSource.Scroll) {
- clickDetector.registerScroll(event.timestamp);
- }
- if (isIncrementalMouseInteraction(event)) {
- const { type, id } = event.data;
- const node = record.mirror.getNode(id);
- if (node instanceof HTMLElement && type === MouseInteractions.Click) {
- clickDetector.registerClick(node);
- }
- }
- } catch (e) {
- // ignore errors here, e.g. if accessing something that does not exist
- }
- }
- function isIncrementalEvent(event) {
- return event.type === ReplayEventTypeIncrementalSnapshot;
- }
- function isIncrementalMouseInteraction(
- event,
- ) {
- return event.data.source === IncrementalSource.MouseInteraction;
- }
- /**
- * Create a breadcrumb for a replay.
- */
- function createBreadcrumb(
- breadcrumb,
- ) {
- return {
- timestamp: Date.now() / 1000,
- type: 'default',
- ...breadcrumb,
- };
- }
- var NodeType;
- (function (NodeType) {
- NodeType[NodeType["Document"] = 0] = "Document";
- NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
- NodeType[NodeType["Element"] = 2] = "Element";
- NodeType[NodeType["Text"] = 3] = "Text";
- NodeType[NodeType["CDATA"] = 4] = "CDATA";
- NodeType[NodeType["Comment"] = 5] = "Comment";
- })(NodeType || (NodeType = {}));
- // Note that these are the serialized attributes and not attributes directly on
- // the DOM Node. Attributes we are interested in:
- const ATTRIBUTES_TO_RECORD = new Set([
- 'id',
- 'class',
- 'aria-label',
- 'role',
- 'name',
- 'alt',
- 'title',
- 'data-test-id',
- 'data-testid',
- 'disabled',
- 'aria-disabled',
- 'data-sentry-component',
- ]);
- /**
- * Inclusion list of attributes that we want to record from the DOM element
- */
- function getAttributesToRecord(attributes) {
- const obj = {};
- for (const key in attributes) {
- if (ATTRIBUTES_TO_RECORD.has(key)) {
- let normalizedKey = key;
- if (key === 'data-testid' || key === 'data-test-id') {
- normalizedKey = 'testId';
- }
- obj[normalizedKey] = attributes[key];
- }
- }
- return obj;
- }
- const handleDomListener = (
- replay,
- ) => {
- return (handlerData) => {
- if (!replay.isEnabled()) {
- return;
- }
- const result = handleDom(handlerData);
- if (!result) {
- return;
- }
- const isClick = handlerData.name === 'click';
- const event = isClick ? (handlerData.event ) : undefined;
- // Ignore clicks if ctrl/alt/meta/shift keys are held down as they alter behavior of clicks (e.g. open in new tab)
- if (
- isClick &&
- replay.clickDetector &&
- event &&
- event.target &&
- !event.altKey &&
- !event.metaKey &&
- !event.ctrlKey &&
- !event.shiftKey
- ) {
- handleClick(
- replay.clickDetector,
- result ,
- getClickTargetNode(handlerData.event ) ,
- );
- }
- addBreadcrumbEvent(replay, result);
- };
- };
- /** Get the base DOM breadcrumb. */
- function getBaseDomBreadcrumb(target, message) {
- const nodeId = record.mirror.getId(target);
- const node = nodeId && record.mirror.getNode(nodeId);
- const meta = node && record.mirror.getMeta(node);
- const element = meta && isElement(meta) ? meta : null;
- return {
- message,
- data: element
- ? {
- nodeId,
- node: {
- id: nodeId,
- tagName: element.tagName,
- textContent: Array.from(element.childNodes)
- .map((node) => node.type === NodeType.Text && node.textContent)
- .filter(Boolean) // filter out empty values
- .map(text => (text ).trim())
- .join(''),
- attributes: getAttributesToRecord(element.attributes),
- },
- }
- : {},
- };
- }
- /**
- * An event handler to react to DOM events.
- * Exported for tests.
- */
- function handleDom(handlerData) {
- const { target, message } = getDomTarget(handlerData);
- return createBreadcrumb({
- category: `ui.${handlerData.name}`,
- ...getBaseDomBreadcrumb(target, message),
- });
- }
- function getDomTarget(handlerData) {
- const isClick = handlerData.name === 'click';
- let message;
- let target = null;
- // Accessing event.target can throw (see getsentry/raven-js#838, #768)
- try {
- target = isClick ? getClickTargetNode(handlerData.event ) : getTargetNode(handlerData.event );
- message = utils.htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
- } catch (e) {
- message = '<unknown>';
- }
- return { target, message };
- }
- function isElement(node) {
- return node.type === NodeType.Element;
- }
- /** Handle keyboard events & create breadcrumbs. */
- function handleKeyboardEvent(replay, event) {
- if (!replay.isEnabled()) {
- return;
- }
- // Update user activity, but do not restart recording as it can create
- // noisy/low-value replays (e.g. user comes back from idle, hits alt-tab, new
- // session with a single "keydown" breadcrumb is created)
- replay.updateUserActivity();
- const breadcrumb = getKeyboardBreadcrumb(event);
- if (!breadcrumb) {
- return;
- }
- addBreadcrumbEvent(replay, breadcrumb);
- }
- /** exported only for tests */
- function getKeyboardBreadcrumb(event) {
- const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event;
- // never capture for input fields
- if (!target || isInputElement(target ) || !key) {
- return null;
- }
- // Note: We do not consider shift here, as that means "uppercase"
- const hasModifierKey = metaKey || ctrlKey || altKey;
- const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length
- // Do not capture breadcrumb if only a word key is pressed
- // This could leak e.g. user input
- if (!hasModifierKey && isCharacterKey) {
- return null;
- }
- const message = utils.htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
- const baseBreadcrumb = getBaseDomBreadcrumb(target , message);
- return createBreadcrumb({
- category: 'ui.keyDown',
- message,
- data: {
- ...baseBreadcrumb.data,
- metaKey,
- shiftKey,
- ctrlKey,
- altKey,
- key,
- },
- });
- }
- function isInputElement(target) {
- return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
- }
- // Map entryType -> function to normalize data for event
- const ENTRY_TYPES
- = {
- // @ts-expect-error TODO: entry type does not fit the create* functions entry type
- resource: createResourceEntry,
- paint: createPaintEntry,
- // @ts-expect-error TODO: entry type does not fit the create* functions entry type
- navigation: createNavigationEntry,
- };
- /**
- * Create replay performance entries from the browser performance entries.
- */
- function createPerformanceEntries(
- entries,
- ) {
- return entries.map(createPerformanceEntry).filter(Boolean) ;
- }
- function createPerformanceEntry(entry) {
- if (!ENTRY_TYPES[entry.entryType]) {
- return null;
- }
- return ENTRY_TYPES[entry.entryType](entry);
- }
- function getAbsoluteTime(time) {
- // browserPerformanceTimeOrigin can be undefined if `performance` or
- // `performance.now` doesn't exist, but this is already checked by this integration
- return ((utils.browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000;
- }
- function createPaintEntry(entry) {
- const { duration, entryType, name, startTime } = entry;
- const start = getAbsoluteTime(startTime);
- return {
- type: entryType,
- name,
- start,
- end: start + duration,
- data: undefined,
- };
- }
- function createNavigationEntry(entry) {
- const {
- entryType,
- name,
- decodedBodySize,
- duration,
- domComplete,
- encodedBodySize,
- domContentLoadedEventStart,
- domContentLoadedEventEnd,
- domInteractive,
- loadEventStart,
- loadEventEnd,
- redirectCount,
- startTime,
- transferSize,
- type,
- } = entry;
- // Ignore entries with no duration, they do not seem to be useful and cause dupes
- if (duration === 0) {
- return null;
- }
- return {
- type: `${entryType}.${type}`,
- start: getAbsoluteTime(startTime),
- end: getAbsoluteTime(domComplete),
- name,
- data: {
- size: transferSize,
- decodedBodySize,
- encodedBodySize,
- duration,
- domInteractive,
- domContentLoadedEventStart,
- domContentLoadedEventEnd,
- loadEventStart,
- loadEventEnd,
- domComplete,
- redirectCount,
- },
- };
- }
- function createResourceEntry(
- entry,
- ) {
- const {
- entryType,
- initiatorType,
- name,
- responseEnd,
- startTime,
- decodedBodySize,
- encodedBodySize,
- responseStatus,
- transferSize,
- } = entry;
- // Core SDK handles these
- if (['fetch', 'xmlhttprequest'].includes(initiatorType)) {
- return null;
- }
- return {
- type: `${entryType}.${initiatorType}`,
- start: getAbsoluteTime(startTime),
- end: getAbsoluteTime(responseEnd),
- name,
- data: {
- size: transferSize,
- statusCode: responseStatus,
- decodedBodySize,
- encodedBodySize,
- },
- };
- }
- /**
- * Add a LCP event to the replay based on an LCP metric.
- */
- function getLargestContentfulPaint(metric
- ) {
- const entries = metric.entries;
- const lastEntry = entries[entries.length - 1] ;
- const element = lastEntry ? lastEntry.element : undefined;
- const value = metric.value;
- const end = getAbsoluteTime(value);
- const data = {
- type: 'largest-contentful-paint',
- name: 'largest-contentful-paint',
- start: end,
- end,
- data: {
- value,
- size: value,
- nodeId: element ? record.mirror.getId(element) : undefined,
- },
- };
- return data;
- }
- /**
- * Sets up a PerformanceObserver to listen to all performance entry types.
- * Returns a callback to stop observing.
- */
- function setupPerformanceObserver(replay) {
- function addPerformanceEntry(entry) {
- // It is possible for entries to come up multiple times
- if (!replay.performanceEntries.includes(entry)) {
- replay.performanceEntries.push(entry);
- }
- }
- function onEntries({ entries }) {
- entries.forEach(addPerformanceEntry);
- }
- const clearCallbacks = [];
- (['navigation', 'paint', 'resource'] ).forEach(type => {
- clearCallbacks.push(tracing.addPerformanceInstrumentationHandler(type, onEntries));
- });
- clearCallbacks.push(
- tracing.addLcpInstrumentationHandler(({ metric }) => {
- replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
- }),
- );
- // A callback to cleanup all handlers
- return () => {
- clearCallbacks.forEach(clearCallback => clearCallback());
- };
- }
- /**
- * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
- *
- * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
- */
- const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
- const r = `var t=Uint8Array,n=Uint16Array,r=Int32Array,e=new t([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),i=new t([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),a=new t([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=function(t,e){for(var i=new n(31),a=0;a<31;++a)i[a]=e+=1<<t[a-1];var s=new r(i[30]);for(a=1;a<30;++a)for(var o=i[a];o<i[a+1];++o)s[o]=o-i[a]<<5|a;return{b:i,r:s}},o=s(e,2),f=o.b,h=o.r;f[28]=258,h[258]=28;for(var l=s(i,0).r,u=new n(32768),c=0;c<32768;++c){var v=(43690&c)>>1|(21845&c)<<1;v=(61680&(v=(52428&v)>>2|(13107&v)<<2))>>4|(3855&v)<<4,u[c]=((65280&v)>>8|(255&v)<<8)>>1}var d=function(t,r,e){for(var i=t.length,a=0,s=new n(r);a<i;++a)t[a]&&++s[t[a]-1];var o,f=new n(r);for(a=1;a<r;++a)f[a]=f[a-1]+s[a-1]<<1;if(e){o=new n(1<<r);var h=15-r;for(a=0;a<i;++a)if(t[a])for(var l=a<<4|t[a],c=r-t[a],v=f[t[a]-1]++<<c,d=v|(1<<c)-1;v<=d;++v)o[u[v]>>h]=l}else for(o=new n(i),a=0;a<i;++a)t[a]&&(o[a]=u[f[t[a]-1]++]>>15-t[a]);return o},g=new t(288);for(c=0;c<144;++c)g[c]=8;for(c=144;c<256;++c)g[c]=9;for(c=256;c<280;++c)g[c]=7;for(c=280;c<288;++c)g[c]=8;var w=new t(32);for(c=0;c<32;++c)w[c]=5;var p=d(g,9,0),y=d(w,5,0),m=function(t){return(t+7)/8|0},b=function(n,r,e){return(null==r||r<0)&&(r=0),(null==e||e>n.length)&&(e=n.length),new t(n.subarray(r,e))},M=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],E=function(t,n,r){var e=new Error(n||M[t]);if(e.code=t,Error.captureStackTrace&&Error.captureStackTrace(e,E),!r)throw e;return e},z=function(t,n,r){r<<=7&n;var e=n/8|0;t[e]|=r,t[e+1]|=r>>8},A=function(t,n,r){r<<=7&n;var e=n/8|0;t[e]|=r,t[e+1]|=r>>8,t[e+2]|=r>>16},_=function(r,e){for(var i=[],a=0;a<r.length;++a)r[a]&&i.push({s:a,f:r[a]});var s=i.length,o=i.slice();if(!s)return{t:F,l:0};if(1==s){var f=new t(i[0].s+1);return f[i[0].s]=1,{t:f,l:1}}i.sort((function(t,n){return t.f-n.f})),i.push({s:-1,f:25001});var h=i[0],l=i[1],u=0,c=1,v=2;for(i[0]={s:-1,f:h.f+l.f,l:h,r:l};c!=s-1;)h=i[i[u].f<i[v].f?u++:v++],l=i[u!=c&&i[u].f<i[v].f?u++:v++],i[c++]={s:-1,f:h.f+l.f,l:h,r:l};var d=o[0].s;for(a=1;a<s;++a)o[a].s>d&&(d=o[a].s);var g=new n(d+1),w=x(i[c-1],g,0);if(w>e){a=0;var p=0,y=w-e,m=1<<y;for(o.sort((function(t,n){return g[n.s]-g[t.s]||t.f-n.f}));a<s;++a){var b=o[a].s;if(!(g[b]>e))break;p+=m-(1<<w-g[b]),g[b]=e}for(p>>=y;p>0;){var M=o[a].s;g[M]<e?p-=1<<e-g[M]++-1:++a}for(;a>=0&&p;--a){var E=o[a].s;g[E]==e&&(--g[E],++p)}w=e}return{t:new t(g),l:w}},x=function(t,n,r){return-1==t.s?Math.max(x(t.l,n,r+1),x(t.r,n,r+1)):n[t.s]=r},D=function(t){for(var r=t.length;r&&!t[--r];);for(var e=new n(++r),i=0,a=t[0],s=1,o=function(t){e[i++]=t},f=1;f<=r;++f)if(t[f]==a&&f!=r)++s;else{if(!a&&s>2){for(;s>138;s-=138)o(32754);s>2&&(o(s>10?s-11<<5|28690:s-3<<5|12305),s=0)}else if(s>3){for(o(a),--s;s>6;s-=6)o(8304);s>2&&(o(s-3<<5|8208),s=0)}for(;s--;)o(a);s=1,a=t[f]}return{c:e.subarray(0,i),n:r}},T=function(t,n){for(var r=0,e=0;e<n.length;++e)r+=t[e]*n[e];return r},k=function(t,n,r){var e=r.length,i=m(n+2);t[i]=255&e,t[i+1]=e>>8,t[i+2]=255^t[i],t[i+3]=255^t[i+1];for(var a=0;a<e;++a)t[i+a+4]=r[a];return 8*(i+4+e)},C=function(t,r,s,o,f,h,l,u,c,v,m){z(r,m++,s),++f[256];for(var b=_(f,15),M=b.t,E=b.l,x=_(h,15),C=x.t,U=x.l,F=D(M),I=F.c,S=F.n,L=D(C),O=L.c,j=L.n,q=new n(19),B=0;B<I.length;++B)++q[31&I[B]];for(B=0;B<O.length;++B)++q[31&O[B]];for(var G=_(q,7),H=G.t,J=G.l,K=19;K>4&&!H[a[K-1]];--K);var N,P,Q,R,V=v+5<<3,W=T(f,g)+T(h,w)+l,X=T(f,M)+T(h,C)+l+14+3*K+T(q,H)+2*q[16]+3*q[17]+7*q[18];if(c>=0&&V<=W&&V<=X)return k(r,m,t.subarray(c,c+v));if(z(r,m,1+(X<W)),m+=2,X<W){N=d(M,E,0),P=M,Q=d(C,U,0),R=C;var Y=d(H,J,0);z(r,m,S-257),z(r,m+5,j-1),z(r,m+10,K-4),m+=14;for(B=0;B<K;++B)z(r,m+3*B,H[a[B]]);m+=3*K;for(var Z=[I,O],$=0;$<2;++$){var tt=Z[$];for(B=0;B<tt.length;++B){var nt=31&tt[B];z(r,m,Y[nt]),m+=H[nt],nt>15&&(z(r,m,tt[B]>>5&127),m+=tt[B]>>12)}}}else N=p,P=g,Q=y,R=w;for(B=0;B<u;++B){var rt=o[B];if(rt>255){A(r,m,N[(nt=rt>>18&31)+257]),m+=P[nt+257],nt>7&&(z(r,m,rt>>23&31),m+=e[nt]);var et=31&rt;A(r,m,Q[et]),m+=R[et],et>3&&(A(r,m,rt>>5&8191),m+=i[et])}else A(r,m,N[rt]),m+=P[rt]}return A(r,m,N[256]),m+P[256]},U=new r([65540,131080,131088,131104,262176,1048704,1048832,2114560,2117632]),F=new t(0),I=function(){for(var t=new Int32Array(256),n=0;n<256;++n){for(var r=n,e=9;--e;)r=(1&r&&-306674912)^r>>>1;t[n]=r}return t}(),S=function(){var t=1,n=0;return{p:function(r){for(var e=t,i=n,a=0|r.length,s=0;s!=a;){for(var o=Math.min(s+2655,a);s<o;++s)i+=e+=r[s];e=(65535&e)+15*(e>>16),i=(65535&i)+15*(i>>16)}t=e,n=i},d:function(){return(255&(t%=65521))<<24|(65280&t)<<8|(255&(n%=65521))<<8|n>>8}}},L=function(a,s,o,f,u){if(!u&&(u={l:1},s.dictionary)){var c=s.dictionary.subarray(-32768),v=new t(c.length+a.length);v.set(c),v.set(a,c.length),a=v,u.w=c.length}return function(a,s,o,f,u,c){var v=c.z||a.length,d=new t(f+v+5*(1+Math.ceil(v/7e3))+u),g=d.subarray(f,d.length-u),w=c.l,p=7&(c.r||0);if(s){p&&(g[0]=c.r>>3);for(var y=U[s-1],M=y>>13,E=8191&y,z=(1<<o)-1,A=c.p||new n(32768),_=c.h||new n(z+1),x=Math.ceil(o/3),D=2*x,T=function(t){return(a[t]^a[t+1]<<x^a[t+2]<<D)&z},F=new r(25e3),I=new n(288),S=new n(32),L=0,O=0,j=c.i||0,q=0,B=c.w||0,G=0;j+2<v;++j){var H=T(j),J=32767&j,K=_[H];if(A[J]=K,_[H]=J,B<=j){var N=v-j;if((L>7e3||q>24576)&&(N>423||!w)){p=C(a,g,0,F,I,S,O,q,G,j-G,p),q=L=O=0,G=j;for(var P=0;P<286;++P)I[P]=0;for(P=0;P<30;++P)S[P]=0}var Q=2,R=0,V=E,W=J-K&32767;if(N>2&&H==T(j-W))for(var X=Math.min(M,N)-1,Y=Math.min(32767,j),Z=Math.min(258,N);W<=Y&&--V&&J!=K;){if(a[j+Q]==a[j+Q-W]){for(var $=0;$<Z&&a[j+$]==a[j+$-W];++$);if($>Q){if(Q=$,R=W,$>X)break;var tt=Math.min(W,$-2),nt=0;for(P=0;P<tt;++P){var rt=j-W+P&32767,et=rt-A[rt]&32767;et>nt&&(nt=et,K=rt)}}}W+=(J=K)-(K=A[J])&32767}if(R){F[q++]=268435456|h[Q]<<18|l[R];var it=31&h[Q],at=31&l[R];O+=e[it]+i[at],++I[257+it],++S[at],B=j+Q,++L}else F[q++]=a[j],++I[a[j]]}}for(j=Math.max(j,B);j<v;++j)F[q++]=a[j],++I[a[j]];p=C(a,g,w,F,I,S,O,q,G,j-G,p),w||(c.r=7&p|g[p/8|0]<<3,p-=7,c.h=_,c.p=A,c.i=j,c.w=B)}else{for(j=c.w||0;j<v+w;j+=65535){var st=j+65535;st>=v&&(g[p/8|0]=w,st=v),p=k(g,p+1,a.subarray(j,st))}c.i=v}return b(d,0,f+m(p)+u)}(a,null==s.level?6:s.level,null==s.mem?Math.ceil(1.5*Math.max(8,Math.min(13,Math.log(a.length)))):12+s.mem,o,f,u)},O=function(t,n,r){for(;r;++n)t[n]=r,r>>>=8},j=function(){function n(n,r){if("function"==typeof n&&(r=n,n={}),this.ondata=r,this.o=n||{},this.s={l:0,i:32768,w:32768,z:32768},this.b=new t(98304),this.o.dictionary){var e=this.o.dictionary.subarray(-32768);this.b.set(e,32768-e.length),this.s.i=32768-e.length}}return n.prototype.p=function(t,n){this.ondata(L(t,this.o,0,0,this.s),n)},n.prototype.push=function(n,r){this.ondata||E(5),this.s.l&&E(4);var e=n.length+this.s.z;if(e>this.b.length){if(e>2*this.b.length-32768){var i=new t(-32768&e);i.set(this.b.subarray(0,this.s.z)),this.b=i}var a=this.b.length-this.s.z;a&&(this.b.set(n.subarray(0,a),this.s.z),this.s.z=this.b.length,this.p(this.b,!1)),this.b.set(this.b.subarray(-32768)),this.b.set(n.subarray(a),32768),this.s.z=n.length-a+32768,this.s.i=32766,this.s.w=32768}else this.b.set(n,this.s.z),this.s.z+=n.length;this.s.l=1&r,(this.s.z>this.s.w+8191||r)&&(this.p(this.b,r||!1),this.s.w=this.s.i,this.s.i-=2)},n}();function q(t,n){n||(n={});var r=function(){var t=-1;return{p:function(n){for(var r=t,e=0;e<n.length;++e)r=I[255&r^n[e]]^r>>>8;t=r},d:function(){return~t}}}(),e=t.length;r.p(t);var i,a=L(t,n,10+((i=n).filename?i.filename.length+1:0),8),s=a.length;return function(t,n){var r=n.filename;if(t[0]=31,t[1]=139,t[2]=8,t[8]=n.level<2?4:9==n.level?2:0,t[9]=3,0!=n.mtime&&O(t,4,Math.floor(new Date(n.mtime||Date.now())/1e3)),r){t[3]=8;for(var e=0;e<=r.length;++e)t[e+10]=r.charCodeAt(e)}}(a,n),O(a,s-8,r.d()),O(a,s-4,e),a}var B=function(){function t(t,n){this.c=S(),this.v=1,j.call(this,t,n)}return t.prototype.push=function(t,n){this.c.p(t),j.prototype.push.call(this,t,n)},t.prototype.p=function(t,n){var r=L(t,this.o,this.v&&(this.o.dictionary?6:2),n&&4,this.s);this.v&&(function(t,n){var r=n.level,e=0==r?0:r<6?1:9==r?3:2;if(t[0]=120,t[1]=e<<6|(n.dictionary&&32),t[1]|=31-(t[0]<<8|t[1])%31,n.dictionary){var i=S();i.p(n.dictionary),O(t,2,i.d())}}(r,this.o),this.v=0),n&&O(r,r.length-4,this.c.d()),this.ondata(r,n)},t}(),G="undefined"!=typeof TextEncoder&&new TextEncoder,H="undefined"!=typeof TextDecoder&&new TextDecoder;try{H.decode(F,{stream:!0})}catch(t){}var J=function(){function t(t){this.ondata=t}return t.prototype.push=function(t,n){this.ondata||E(5),this.d&&E(4),this.ondata(K(t),this.d=n||!1)},t}();function K(n,r){if(r){for(var e=new t(n.length),i=0;i<n.length;++i)e[i]=n.charCodeAt(i);return e}if(G)return G.encode(n);var a=n.length,s=new t(n.length+(n.length>>1)),o=0,f=function(t){s[o++]=t};for(i=0;i<a;++i){if(o+5>s.length){var h=new t(o+8+(a-i<<1));h.set(s),s=h}var l=n.charCodeAt(i);l<128||r?f(l):l<2048?(f(192|l>>6),f(128|63&l)):l>55295&&l<57344?(f(240|(l=65536+(1047552&l)|1023&n.charCodeAt(++i))>>18),f(128|l>>12&63),f(128|l>>6&63),f(128|63&l)):(f(224|l>>12),f(128|l>>6&63),f(128|63&l))}return b(s,0,o)}const N=new class{constructor(){this._init()}clear(){this._init()}addEvent(t){if(!t)throw new Error("Adding invalid event");const n=this._hasEvents?",":"";this.stream.push(n+t),this._hasEvents=!0}finish(){this.stream.push("]",!0);const t=function(t){let n=0;for(let r=0,e=t.length;r<e;r++)n+=t[r].length;const r=new Uint8Array(n);for(let n=0,e=0,i=t.length;n<i;n++){const i=t[n];r.set(i,e),e+=i.length}return r}(this._deflatedData);return this._init(),t}_init(){this._hasEvents=!1,this._deflatedData=[],this.deflate=new B,this.deflate.ondata=(t,n)=>{this._deflatedData.push(t)},this.stream=new J(((t,n)=>{this.deflate.push(t,n)})),this.stream.push("[")}},P={clear:()=>{N.clear()},addEvent:t=>N.addEvent(t),finish:()=>N.finish(),compress:t=>function(t){return q(K(t))}(t)};addEventListener("message",(function(t){const n=t.data.method,r=t.data.id,e=t.data.arg;if(n in P&&"function"==typeof P[n])try{const t=P[n](e);postMessage({id:r,method:n,success:!0,response:t})}catch(t){postMessage({id:r,method:n,success:!1,response:t.message}),console.error(t)}})),postMessage({id:void 0,method:"init",success:!0,response:void 0});`;
- function e(){const e=new Blob([r]);return URL.createObjectURL(e)}
- /**
- * Log a message in debug mode, and add a breadcrumb when _experiment.traceInternals is enabled.
- */
- function logInfo(message, shouldAddBreadcrumb) {
- if (!DEBUG_BUILD) {
- return;
- }
- utils.logger.info(message);
- if (shouldAddBreadcrumb) {
- addLogBreadcrumb(message);
- }
- }
- /**
- * Log a message, and add a breadcrumb in the next tick.
- * This is necessary when the breadcrumb may be added before the replay is initialized.
- */
- function logInfoNextTick(message, shouldAddBreadcrumb) {
- if (!DEBUG_BUILD) {
- return;
- }
- utils.logger.info(message);
- if (shouldAddBreadcrumb) {
- // Wait a tick here to avoid race conditions for some initial logs
- // which may be added before replay is initialized
- setTimeout(() => {
- addLogBreadcrumb(message);
- }, 0);
- }
- }
- function addLogBreadcrumb(message) {
- core.addBreadcrumb(
- {
- category: 'console',
- data: {
- logger: 'replay',
- },
- level: 'info',
- message,
- },
- { level: 'info' },
- );
- }
- /** This error indicates that the event buffer size exceeded the limit.. */
- class EventBufferSizeExceededError extends Error {
- constructor() {
- super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`);
- }
- }
- /**
- * A basic event buffer that does not do any compression.
- * Used as fallback if the compression worker cannot be loaded or is disabled.
- */
- class EventBufferArray {
- /** All the events that are buffered to be sent. */
- /** @inheritdoc */
- constructor() {
- this.events = [];
- this._totalSize = 0;
- this.hasCheckout = false;
- }
- /** @inheritdoc */
- get hasEvents() {
- return this.events.length > 0;
- }
- /** @inheritdoc */
- get type() {
- return 'sync';
- }
- /** @inheritdoc */
- destroy() {
- this.events = [];
- }
- /** @inheritdoc */
- async addEvent(event) {
- const eventSize = JSON.stringify(event).length;
- this._totalSize += eventSize;
- if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
- throw new EventBufferSizeExceededError();
- }
- this.events.push(event);
- }
- /** @inheritdoc */
- finish() {
- return new Promise(resolve => {
- // Make a copy of the events array reference and immediately clear the
- // events member so that we do not lose new events while uploading
- // attachment.
- const eventsRet = this.events;
- this.clear();
- resolve(JSON.stringify(eventsRet));
- });
- }
- /** @inheritdoc */
- clear() {
- this.events = [];
- this._totalSize = 0;
- this.hasCheckout = false;
- }
- /** @inheritdoc */
- getEarliestTimestamp() {
- const timestamp = this.events.map(event => event.timestamp).sort()[0];
- if (!timestamp) {
- return null;
- }
- return timestampToMs(timestamp);
- }
- }
- /**
- * Event buffer that uses a web worker to compress events.
- * Exported only for testing.
- */
- class WorkerHandler {
- constructor(worker) {
- this._worker = worker;
- this._id = 0;
- }
- /**
- * Ensure the worker is ready (or not).
- * This will either resolve when the worker is ready, or reject if an error occured.
- */
- ensureReady() {
- // Ensure we only check once
- if (this._ensureReadyPromise) {
- return this._ensureReadyPromise;
- }
- this._ensureReadyPromise = new Promise((resolve, reject) => {
- this._worker.addEventListener(
- 'message',
- ({ data }) => {
- if ((data ).success) {
- resolve();
- } else {
- reject();
- }
- },
- { once: true },
- );
- this._worker.addEventListener(
- 'error',
- error => {
- reject(error);
- },
- { once: true },
- );
- });
- return this._ensureReadyPromise;
- }
- /**
- * Destroy the worker.
- */
- destroy() {
- logInfo('[Replay] Destroying compression worker');
- this._worker.terminate();
- }
- /**
- * Post message to worker and wait for response before resolving promise.
- */
- postMessage(method, arg) {
- const id = this._getAndIncrementId();
- return new Promise((resolve, reject) => {
- const listener = ({ data }) => {
- const response = data ;
- if (response.method !== method) {
- return;
- }
- // There can be multiple listeners for a single method, the id ensures
- // that the response matches the caller.
- if (response.id !== id) {
- return;
- }
- // At this point, we'll always want to remove listener regardless of result status
- this._worker.removeEventListener('message', listener);
- if (!response.success) {
- // TODO: Do some error handling, not sure what
- DEBUG_BUILD && utils.logger.error('[Replay]', response.response);
- reject(new Error('Error in compression worker'));
- return;
- }
- resolve(response.response );
- };
- // Note: we can't use `once` option because it's possible it needs to
- // listen to multiple messages
- this._worker.addEventListener('message', listener);
- this._worker.postMessage({ id, method, arg });
- });
- }
- /** Get the current ID and increment it for the next call. */
- _getAndIncrementId() {
- return this._id++;
- }
- }
- /**
- * Event buffer that uses a web worker to compress events.
- * Exported only for testing.
- */
- class EventBufferCompressionWorker {
- /** @inheritdoc */
- constructor(worker) {
- this._worker = new WorkerHandler(worker);
- this._earliestTimestamp = null;
- this._totalSize = 0;
- this.hasCheckout = false;
- }
- /** @inheritdoc */
- get hasEvents() {
- return !!this._earliestTimestamp;
- }
- /** @inheritdoc */
- get type() {
- return 'worker';
- }
- /**
- * Ensure the worker is ready (or not).
- * This will either resolve when the worker is ready, or reject if an error occured.
- */
- ensureReady() {
- return this._worker.ensureReady();
- }
- /**
- * Destroy the event buffer.
- */
- destroy() {
- this._worker.destroy();
- }
- /**
- * Add an event to the event buffer.
- *
- * Returns true if event was successfuly received and processed by worker.
- */
- addEvent(event) {
- const timestamp = timestampToMs(event.timestamp);
- if (!this._earliestTimestamp || timestamp < this._earliestTimestamp) {
- this._earliestTimestamp = timestamp;
- }
- const data = JSON.stringify(event);
- this._totalSize += data.length;
- if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
- return Promise.reject(new EventBufferSizeExceededError());
- }
- return this._sendEventToWorker(data);
- }
- /**
- * Finish the event buffer and return the compressed data.
- */
- finish() {
- return this._finishRequest();
- }
- /** @inheritdoc */
- clear() {
- this._earliestTimestamp = null;
- this._totalSize = 0;
- this.hasCheckout = false;
- // We do not wait on this, as we assume the order of messages is consistent for the worker
- this._worker.postMessage('clear').then(null, e => {
- DEBUG_BUILD && utils.logger.warn('[Replay] Sending "clear" message to worker failed', e);
- });
- }
- /** @inheritdoc */
- getEarliestTimestamp() {
- return this._earliestTimestamp;
- }
- /**
- * Send the event to the worker.
- */
- _sendEventToWorker(data) {
- return this._worker.postMessage('addEvent', data);
- }
- /**
- * Finish the request and return the compressed data from the worker.
- */
- async _finishRequest() {
- const response = await this._worker.postMessage('finish');
- this._earliestTimestamp = null;
- this._totalSize = 0;
- return response;
- }
- }
- /**
- * This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
- * This can happen e.g. if the worker cannot be loaded.
- * Exported only for testing.
- */
- class EventBufferProxy {
- constructor(worker) {
- this._fallback = new EventBufferArray();
- this._compression = new EventBufferCompressionWorker(worker);
- this._used = this._fallback;
- this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded();
- }
- /** @inheritdoc */
- get type() {
- return this._used.type;
- }
- /** @inheritDoc */
- get hasEvents() {
- return this._used.hasEvents;
- }
- /** @inheritdoc */
- get hasCheckout() {
- return this._used.hasCheckout;
- }
- /** @inheritdoc */
- set hasCheckout(value) {
- this._used.hasCheckout = value;
- }
- /** @inheritDoc */
- destroy() {
- this._fallback.destroy();
- this._compression.destroy();
- }
- /** @inheritdoc */
- clear() {
- return this._used.clear();
- }
- /** @inheritdoc */
- getEarliestTimestamp() {
- return this._used.getEarliestTimestamp();
- }
- /**
- * Add an event to the event buffer.
- *
- * Returns true if event was successfully added.
- */
- addEvent(event) {
- return this._used.addEvent(event);
- }
- /** @inheritDoc */
- async finish() {
- // Ensure the worker is loaded, so the sent event is compressed
- await this.ensureWorkerIsLoaded();
- return this._used.finish();
- }
- /** Ensure the worker has loaded. */
- ensureWorkerIsLoaded() {
- return this._ensureWorkerIsLoadedPromise;
- }
- /** Actually check if the worker has been loaded. */
- async _ensureWorkerIsLoaded() {
- try {
- await this._compression.ensureReady();
- } catch (error) {
- // If the worker fails to load, we fall back to the simple buffer.
- // Nothing more to do from our side here
- logInfo('[Replay] Failed to load the compression worker, falling back to simple buffer');
- return;
- }
- // Now we need to switch over the array buffer to the compression worker
- await this._switchToCompressionWorker();
- }
- /** Switch the used buffer to the compression worker. */
- async _switchToCompressionWorker() {
- const { events, hasCheckout } = this._fallback;
- const addEventPromises = [];
- for (const event of events) {
- addEventPromises.push(this._compression.addEvent(event));
- }
- this._compression.hasCheckout = hasCheckout;
- // We switch over to the new buffer immediately - any further events will be added
- // after the previously buffered ones
- this._used = this._compression;
- // Wait for original events to be re-added before resolving
- try {
- await Promise.all(addEventPromises);
- } catch (error) {
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to add events when switching buffers.', error);
- }
- }
- }
- /**
- * Create an event buffer for replays.
- */
- function createEventBuffer({
- useCompression,
- workerUrl: customWorkerUrl,
- }) {
- if (
- useCompression &&
- // eslint-disable-next-line no-restricted-globals
- window.Worker
- ) {
- const worker = _loadWorker(customWorkerUrl);
- if (worker) {
- return worker;
- }
- }
- logInfo('[Replay] Using simple buffer');
- return new EventBufferArray();
- }
- function _loadWorker(customWorkerUrl) {
- try {
- const workerUrl = customWorkerUrl || _getWorkerUrl();
- if (!workerUrl) {
- return;
- }
- logInfo(`[Replay] Using compression worker${customWorkerUrl ? ` from ${customWorkerUrl}` : ''}`);
- const worker = new Worker(workerUrl);
- return new EventBufferProxy(worker);
- } catch (error) {
- logInfo('[Replay] Failed to create compression worker');
- // Fall back to use simple event buffer array
- }
- }
- function _getWorkerUrl() {
- if (typeof __SENTRY_EXCLUDE_REPLAY_WORKER__ === 'undefined' || !__SENTRY_EXCLUDE_REPLAY_WORKER__) {
- return e();
- }
- return '';
- }
- /** If sessionStorage is available. */
- function hasSessionStorage() {
- try {
- // This can throw, e.g. when being accessed in a sandboxed iframe
- return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage;
- } catch (e) {
- return false;
- }
- }
- /**
- * Removes the session from Session Storage and unsets session in replay instance
- */
- function clearSession(replay) {
- deleteSession();
- replay.session = undefined;
- }
- /**
- * Deletes a session from storage
- */
- function deleteSession() {
- if (!hasSessionStorage()) {
- return;
- }
- try {
- WINDOW.sessionStorage.removeItem(REPLAY_SESSION_KEY);
- } catch (e) {
- // Ignore potential SecurityError exceptions
- }
- }
- /**
- * Given a sample rate, returns true if replay should be sampled.
- *
- * 1.0 = 100% sampling
- * 0.0 = 0% sampling
- */
- function isSampled(sampleRate) {
- if (sampleRate === undefined) {
- return false;
- }
- // Math.random() returns a number in range of 0 to 1 (inclusive of 0, but not 1)
- return Math.random() < sampleRate;
- }
- /**
- * Get a session with defaults & applied sampling.
- */
- function makeSession(session) {
- const now = Date.now();
- const id = session.id || utils.uuid4();
- // Note that this means we cannot set a started/lastActivity of `0`, but this should not be relevant outside of tests.
- const started = session.started || now;
- const lastActivity = session.lastActivity || now;
- const segmentId = session.segmentId || 0;
- const sampled = session.sampled;
- const previousSessionId = session.previousSessionId;
- return {
- id,
- started,
- lastActivity,
- segmentId,
- sampled,
- previousSessionId,
- };
- }
- /**
- * Save a session to session storage.
- */
- function saveSession(session) {
- if (!hasSessionStorage()) {
- return;
- }
- try {
- WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session));
- } catch (e) {
- // Ignore potential SecurityError exceptions
- }
- }
- /**
- * Get the sampled status for a session based on sample rates & current sampled status.
- */
- function getSessionSampleType(sessionSampleRate, allowBuffering) {
- return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false;
- }
- /**
- * Create a new session, which in its current implementation is a Sentry event
- * that all replays will be saved to as attachments. Currently, we only expect
- * one of these Sentry events per "replay session".
- */
- function createSession(
- { sessionSampleRate, allowBuffering, stickySession = false },
- { previousSessionId } = {},
- ) {
- const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
- const session = makeSession({
- sampled,
- previousSessionId,
- });
- if (stickySession) {
- saveSession(session);
- }
- return session;
- }
- /**
- * Fetches a session from storage
- */
- function fetchSession(traceInternals) {
- if (!hasSessionStorage()) {
- return null;
- }
- try {
- // This can throw if cookies are disabled
- const sessionStringFromStorage = WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY);
- if (!sessionStringFromStorage) {
- return null;
- }
- const sessionObj = JSON.parse(sessionStringFromStorage) ;
- logInfoNextTick('[Replay] Loading existing session', traceInternals);
- return makeSession(sessionObj);
- } catch (e) {
- return null;
- }
- }
- /**
- * Given an initial timestamp and an expiry duration, checks to see if current
- * time should be considered as expired.
- */
- function isExpired(
- initialTime,
- expiry,
- targetTime = +new Date(),
- ) {
- // Always expired if < 0
- if (initialTime === null || expiry === undefined || expiry < 0) {
- return true;
- }
- // Never expires if == 0
- if (expiry === 0) {
- return false;
- }
- return initialTime + expiry <= targetTime;
- }
- /**
- * Checks to see if session is expired
- */
- function isSessionExpired(
- session,
- {
- maxReplayDuration,
- sessionIdleExpire,
- targetTime = Date.now(),
- },
- ) {
- return (
- // First, check that maximum session length has not been exceeded
- isExpired(session.started, maxReplayDuration, targetTime) ||
- // check that the idle timeout has not been exceeded (i.e. user has
- // performed an action within the last `sessionIdleExpire` ms)
- isExpired(session.lastActivity, sessionIdleExpire, targetTime)
- );
- }
- /** If the session should be refreshed or not. */
- function shouldRefreshSession(
- session,
- { sessionIdleExpire, maxReplayDuration },
- ) {
- // If not expired, all good, just keep the session
- if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) {
- return false;
- }
- // If we are buffering & haven't ever flushed yet, always continue
- if (session.sampled === 'buffer' && session.segmentId === 0) {
- return false;
- }
- return true;
- }
- /**
- * Get or create a session, when initializing the replay.
- * Returns a session that may be unsampled.
- */
- function loadOrCreateSession(
- {
- traceInternals,
- sessionIdleExpire,
- maxReplayDuration,
- previousSessionId,
- }
- ,
- sessionOptions,
- ) {
- const existingSession = sessionOptions.stickySession && fetchSession(traceInternals);
- // No session exists yet, just create a new one
- if (!existingSession) {
- logInfoNextTick('[Replay] Creating new session', traceInternals);
- return createSession(sessionOptions, { previousSessionId });
- }
- if (!shouldRefreshSession(existingSession, { sessionIdleExpire, maxReplayDuration })) {
- return existingSession;
- }
- logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...');
- return createSession(sessionOptions, { previousSessionId: existingSession.id });
- }
- function isCustomEvent(event) {
- return event.type === EventType.Custom;
- }
- /**
- * Add an event to the event buffer.
- * In contrast to `addEvent`, this does not return a promise & does not wait for the adding of the event to succeed/fail.
- * Instead this returns `true` if we tried to add the event, else false.
- * It returns `false` e.g. if we are paused, disabled, or out of the max replay duration.
- *
- * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
- */
- function addEventSync(replay, event, isCheckout) {
- if (!shouldAddEvent(replay, event)) {
- return false;
- }
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- _addEvent(replay, event, isCheckout);
- return true;
- }
- /**
- * Add an event to the event buffer.
- * Resolves to `null` if no event was added, else to `void`.
- *
- * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
- */
- function addEvent(
- replay,
- event,
- isCheckout,
- ) {
- if (!shouldAddEvent(replay, event)) {
- return Promise.resolve(null);
- }
- return _addEvent(replay, event, isCheckout);
- }
- async function _addEvent(
- replay,
- event,
- isCheckout,
- ) {
- if (!replay.eventBuffer) {
- return null;
- }
- try {
- if (isCheckout && replay.recordingMode === 'buffer') {
- replay.eventBuffer.clear();
- }
- if (isCheckout) {
- replay.eventBuffer.hasCheckout = true;
- }
- const replayOptions = replay.getOptions();
- const eventAfterPossibleCallback = maybeApplyCallback(event, replayOptions.beforeAddRecordingEvent);
- if (!eventAfterPossibleCallback) {
- return;
- }
- return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
- } catch (error) {
- const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
- DEBUG_BUILD && utils.logger.error(error);
- await replay.stop({ reason });
- const client = core.getClient();
- if (client) {
- client.recordDroppedEvent('internal_sdk_error', 'replay');
- }
- }
- }
- /** Exported only for tests. */
- function shouldAddEvent(replay, event) {
- if (!replay.eventBuffer || replay.isPaused() || !replay.isEnabled()) {
- return false;
- }
- const timestampInMs = timestampToMs(event.timestamp);
- // Throw out events that happen more than 5 minutes ago. This can happen if
- // page has been left open and idle for a long period of time and user
- // comes back to trigger a new session. The performance entries rely on
- // `performance.timeOrigin`, which is when the page first opened.
- if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
- return false;
- }
- // Throw out events that are +60min from the initial timestamp
- if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) {
- logInfo(
- `[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`,
- replay.getOptions()._experiments.traceInternals,
- );
- return false;
- }
- return true;
- }
- function maybeApplyCallback(
- event,
- callback,
- ) {
- try {
- if (typeof callback === 'function' && isCustomEvent(event)) {
- return callback(event);
- }
- } catch (error) {
- DEBUG_BUILD &&
- utils.logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error);
- return null;
- }
- return event;
- }
- /** If the event is an error event */
- function isErrorEvent(event) {
- return !event.type;
- }
- /** If the event is a transaction event */
- function isTransactionEvent(event) {
- return event.type === 'transaction';
- }
- /** If the event is an replay event */
- function isReplayEvent(event) {
- return event.type === 'replay_event';
- }
- /** If the event is a feedback event */
- function isFeedbackEvent(event) {
- return event.type === 'feedback';
- }
- /**
- * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`.
- */
- function handleAfterSendEvent(replay) {
- // Custom transports may still be returning `Promise<void>`, which means we cannot expect the status code to be available there
- // TODO (v8): remove this check as it will no longer be necessary
- const enforceStatusCode = isBaseTransportSend();
- return (event, sendResponse) => {
- if (!replay.isEnabled() || (!isErrorEvent(event) && !isTransactionEvent(event))) {
- return;
- }
- const statusCode = sendResponse && sendResponse.statusCode;
- // We only want to do stuff on successful error sending, otherwise you get error replays without errors attached
- // If not using the base transport, we allow `undefined` response (as a custom transport may not implement this correctly yet)
- // If we do use the base transport, we skip if we encountered an non-OK status code
- if (enforceStatusCode && (!statusCode || statusCode < 200 || statusCode >= 300)) {
- return;
- }
- if (isTransactionEvent(event)) {
- handleTransactionEvent(replay, event);
- return;
- }
- handleErrorEvent(replay, event);
- };
- }
- function handleTransactionEvent(replay, event) {
- const replayContext = replay.getContext();
- // Collect traceIds in _context regardless of `recordingMode`
- // In error mode, _context gets cleared on every checkout
- // We limit to max. 100 transactions linked
- if (event.contexts && event.contexts.trace && event.contexts.trace.trace_id && replayContext.traceIds.size < 100) {
- replayContext.traceIds.add(event.contexts.trace.trace_id );
- }
- }
- function handleErrorEvent(replay, event) {
- const replayContext = replay.getContext();
- // Add error to list of errorIds of replay. This is ok to do even if not
- // sampled because context will get reset at next checkout.
- // XXX: There is also a race condition where it's possible to capture an
- // error to Sentry before Replay SDK has loaded, but response returns after
- // it was loaded, and this gets called.
- // We limit to max. 100 errors linked
- if (event.event_id && replayContext.errorIds.size < 100) {
- replayContext.errorIds.add(event.event_id);
- }
- // If error event is tagged with replay id it means it was sampled (when in buffer mode)
- // Need to be very careful that this does not cause an infinite loop
- if (replay.recordingMode !== 'buffer' || !event.tags || !event.tags.replayId) {
- return;
- }
- const { beforeErrorSampling } = replay.getOptions();
- if (typeof beforeErrorSampling === 'function' && !beforeErrorSampling(event)) {
- return;
- }
- setTimeout(() => {
- // Capture current event buffer as new replay
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- replay.sendBufferedReplayOrFlush();
- });
- }
- function isBaseTransportSend() {
- const client = core.getClient();
- if (!client) {
- return false;
- }
- const transport = client.getTransport();
- if (!transport) {
- return false;
- }
- return (
- (transport.send ).__sentry__baseTransport__ || false
- );
- }
- /**
- * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`.
- */
- function handleBeforeSendEvent(replay) {
- return (event) => {
- if (!replay.isEnabled() || !isErrorEvent(event)) {
- return;
- }
- handleHydrationError(replay, event);
- };
- }
- function handleHydrationError(replay, event) {
- const exceptionValue = event.exception && event.exception.values && event.exception.values[0].value;
- if (typeof exceptionValue !== 'string') {
- return;
- }
- if (
- // Only matches errors in production builds of react-dom
- // Example https://reactjs.org/docs/error-decoder.html?invariant=423
- exceptionValue.match(/reactjs\.org\/docs\/error-decoder\.html\?invariant=(418|419|422|423|425)/) ||
- // Development builds of react-dom
- // Error 1: Hydration failed because the initial UI does not match what was rendered on the server.
- // Error 2: Text content does not match server-rendered HTML. Warning: Text content did not match.
- exceptionValue.match(/(does not match server-rendered HTML|Hydration failed because)/i)
- ) {
- const breadcrumb = createBreadcrumb({
- category: 'replay.hydrate-error',
- });
- addBreadcrumbEvent(replay, breadcrumb);
- }
- }
- /**
- * Returns true if we think the given event is an error originating inside of rrweb.
- */
- function isRrwebError(event, hint) {
- if (event.type || !event.exception || !event.exception.values || !event.exception.values.length) {
- return false;
- }
- // @ts-expect-error this may be set by rrweb when it finds errors
- if (hint.originalException && hint.originalException.__rrweb__) {
- return true;
- }
- return false;
- }
- /**
- * Add a feedback breadcrumb event to replay.
- */
- function addFeedbackBreadcrumb(replay, event) {
- replay.triggerUserActivity();
- replay.addUpdate(() => {
- if (!event.timestamp) {
- // Ignore events that don't have timestamps (this shouldn't happen, more of a typing issue)
- // Return true here so that we don't flush
- return true;
- }
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- replay.throttledAddEvent({
- type: EventType.Custom,
- timestamp: event.timestamp * 1000,
- data: {
- tag: 'breadcrumb',
- payload: {
- timestamp: event.timestamp,
- type: 'default',
- category: 'sentry.feedback',
- data: {
- feedbackId: event.event_id,
- },
- },
- },
- } );
- return false;
- });
- }
- /**
- * Determine if event should be sampled (only applies in buffer mode).
- * When an event is captured by `hanldleGlobalEvent`, when in buffer mode
- * we determine if we want to sample the error or not.
- */
- function shouldSampleForBufferEvent(replay, event) {
- if (replay.recordingMode !== 'buffer') {
- return false;
- }
- // ignore this error because otherwise we could loop indefinitely with
- // trying to capture replay and failing
- if (event.message === UNABLE_TO_SEND_REPLAY) {
- return false;
- }
- // Require the event to be an error event & to have an exception
- if (!event.exception || event.type) {
- return false;
- }
- return isSampled(replay.getOptions().errorSampleRate);
- }
- /**
- * Returns a listener to be added to `addEventProcessor(listener)`.
- */
- function handleGlobalEventListener(
- replay,
- includeAfterSendEventHandling = false,
- ) {
- const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined;
- return Object.assign(
- (event, hint) => {
- // Do nothing if replay has been disabled
- if (!replay.isEnabled()) {
- return event;
- }
- if (isReplayEvent(event)) {
- // Replays have separate set of breadcrumbs, do not include breadcrumbs
- // from core SDK
- delete event.breadcrumbs;
- return event;
- }
- // We only want to handle errors, transactions, and feedbacks, nothing else
- if (!isErrorEvent(event) && !isTransactionEvent(event) && !isFeedbackEvent(event)) {
- return event;
- }
- // Ensure we do not add replay_id if the session is expired
- const isSessionActive = replay.checkAndHandleExpiredSession();
- if (!isSessionActive) {
- return event;
- }
- if (isFeedbackEvent(event)) {
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- replay.flush();
- event.contexts.feedback.replay_id = replay.getSessionId();
- // Add a replay breadcrumb for this piece of feedback
- addFeedbackBreadcrumb(replay, event);
- return event;
- }
- // Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb
- // As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users
- if (isRrwebError(event, hint) && !replay.getOptions()._experiments.captureExceptions) {
- DEBUG_BUILD && utils.logger.log('[Replay] Ignoring error from rrweb internals', event);
- return null;
- }
- // When in buffer mode, we decide to sample here.
- // Later, in `handleAfterSendEvent`, if the replayId is set, we know that we sampled
- // And convert the buffer session to a full session
- const isErrorEventSampled = shouldSampleForBufferEvent(replay, event);
- // Tag errors if it has been sampled in buffer mode, or if it is session mode
- // Only tag transactions if in session mode
- const shouldTagReplayId = isErrorEventSampled || replay.recordingMode === 'session';
- if (shouldTagReplayId) {
- event.tags = { ...event.tags, replayId: replay.getSessionId() };
- }
- // In cases where a custom client is used that does not support the new hooks (yet),
- // we manually call this hook method here
- if (afterSendHandler) {
- // Pretend the error had a 200 response so we always capture it
- afterSendHandler(event, { statusCode: 200 });
- }
- return event;
- },
- { id: 'Replay' },
- );
- }
- /**
- * Create a "span" for each performance entry.
- */
- function createPerformanceSpans(
- replay,
- entries,
- ) {
- return entries.map(({ type, start, end, name, data }) => {
- const response = replay.throttledAddEvent({
- type: EventType.Custom,
- timestamp: start,
- data: {
- tag: 'performanceSpan',
- payload: {
- op: type,
- description: name,
- startTimestamp: start,
- endTimestamp: end,
- data,
- },
- },
- });
- // If response is a string, it means its either THROTTLED or SKIPPED
- return typeof response === 'string' ? Promise.resolve(null) : response;
- });
- }
- function handleHistory(handlerData) {
- const { from, to } = handlerData;
- const now = Date.now() / 1000;
- return {
- type: 'navigation.push',
- start: now,
- end: now,
- name: to,
- data: {
- previous: from,
- },
- };
- }
- /**
- * Returns a listener to be added to `addHistoryInstrumentationHandler(listener)`.
- */
- function handleHistorySpanListener(replay) {
- return (handlerData) => {
- if (!replay.isEnabled()) {
- return;
- }
- const result = handleHistory(handlerData);
- if (result === null) {
- return;
- }
- // Need to collect visited URLs
- replay.getContext().urls.push(result.name);
- replay.triggerUserActivity();
- replay.addUpdate(() => {
- createPerformanceSpans(replay, [result]);
- // Returning false to flush
- return false;
- });
- };
- }
- /**
- * Check whether a given request URL should be filtered out. This is so we
- * don't log Sentry ingest requests.
- */
- function shouldFilterRequest(replay, url) {
- // If we enabled the `traceInternals` experiment, we want to trace everything
- if (DEBUG_BUILD && replay.getOptions()._experiments.traceInternals) {
- return false;
- }
- return core.isSentryRequestUrl(url, core.getClient());
- }
- /** Add a performance entry breadcrumb */
- function addNetworkBreadcrumb(
- replay,
- result,
- ) {
- if (!replay.isEnabled()) {
- return;
- }
- if (result === null) {
- return;
- }
- if (shouldFilterRequest(replay, result.name)) {
- return;
- }
- replay.addUpdate(() => {
- createPerformanceSpans(replay, [result]);
- // Returning true will cause `addUpdate` to not flush
- // We do not want network requests to cause a flush. This will prevent
- // recurring/polling requests from keeping the replay session alive.
- return true;
- });
- }
- /** only exported for tests */
- function handleFetch(handlerData) {
- const { startTimestamp, endTimestamp, fetchData, response } = handlerData;
- if (!endTimestamp) {
- return null;
- }
- // This is only used as a fallback, so we know the body sizes are never set here
- const { method, url } = fetchData;
- return {
- type: 'resource.fetch',
- start: startTimestamp / 1000,
- end: endTimestamp / 1000,
- name: url,
- data: {
- method,
- statusCode: response ? (response ).status : undefined,
- },
- };
- }
- /**
- * Returns a listener to be added to `addFetchInstrumentationHandler(listener)`.
- */
- function handleFetchSpanListener(replay) {
- return (handlerData) => {
- if (!replay.isEnabled()) {
- return;
- }
- const result = handleFetch(handlerData);
- addNetworkBreadcrumb(replay, result);
- };
- }
- /** only exported for tests */
- function handleXhr(handlerData) {
- const { startTimestamp, endTimestamp, xhr } = handlerData;
- const sentryXhrData = xhr[utils.SENTRY_XHR_DATA_KEY];
- if (!startTimestamp || !endTimestamp || !sentryXhrData) {
- return null;
- }
- // This is only used as a fallback, so we know the body sizes are never set here
- const { method, url, status_code: statusCode } = sentryXhrData;
- if (url === undefined) {
- return null;
- }
- return {
- type: 'resource.xhr',
- name: url,
- start: startTimestamp / 1000,
- end: endTimestamp / 1000,
- data: {
- method,
- statusCode,
- },
- };
- }
- /**
- * Returns a listener to be added to `addXhrInstrumentationHandler(listener)`.
- */
- function handleXhrSpanListener(replay) {
- return (handlerData) => {
- if (!replay.isEnabled()) {
- return;
- }
- const result = handleXhr(handlerData);
- addNetworkBreadcrumb(replay, result);
- };
- }
- /** Get the size of a body. */
- function getBodySize(
- body,
- textEncoder,
- ) {
- if (!body) {
- return undefined;
- }
- try {
- if (typeof body === 'string') {
- return textEncoder.encode(body).length;
- }
- if (body instanceof URLSearchParams) {
- return textEncoder.encode(body.toString()).length;
- }
- if (body instanceof FormData) {
- const formDataStr = _serializeFormData(body);
- return textEncoder.encode(formDataStr).length;
- }
- if (body instanceof Blob) {
- return body.size;
- }
- if (body instanceof ArrayBuffer) {
- return body.byteLength;
- }
- // Currently unhandled types: ArrayBufferView, ReadableStream
- } catch (e) {
- // just return undefined
- }
- return undefined;
- }
- /** Convert a Content-Length header to number/undefined. */
- function parseContentLengthHeader(header) {
- if (!header) {
- return undefined;
- }
- const size = parseInt(header, 10);
- return isNaN(size) ? undefined : size;
- }
- /** Get the string representation of a body. */
- function getBodyString(body) {
- try {
- if (typeof body === 'string') {
- return [body];
- }
- if (body instanceof URLSearchParams) {
- return [body.toString()];
- }
- if (body instanceof FormData) {
- return [_serializeFormData(body)];
- }
- if (!body) {
- return [undefined];
- }
- } catch (e2) {
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize body', body);
- return [undefined, 'BODY_PARSE_ERROR'];
- }
- DEBUG_BUILD && utils.logger.info('[Replay] Skipping network body because of body type', body);
- return [undefined, 'UNPARSEABLE_BODY_TYPE'];
- }
- /** Merge a warning into an existing network request/response. */
- function mergeWarning(
- info,
- warning,
- ) {
- if (!info) {
- return {
- headers: {},
- size: undefined,
- _meta: {
- warnings: [warning],
- },
- };
- }
- const newMeta = { ...info._meta };
- const existingWarnings = newMeta.warnings || [];
- newMeta.warnings = [...existingWarnings, warning];
- info._meta = newMeta;
- return info;
- }
- /** Convert ReplayNetworkRequestData to a PerformanceEntry. */
- function makeNetworkReplayBreadcrumb(
- type,
- data,
- ) {
- if (!data) {
- return null;
- }
- const { startTimestamp, endTimestamp, url, method, statusCode, request, response } = data;
- const result = {
- type,
- start: startTimestamp / 1000,
- end: endTimestamp / 1000,
- name: url,
- data: utils.dropUndefinedKeys({
- method,
- statusCode,
- request,
- response,
- }),
- };
- return result;
- }
- /** Build the request or response part of a replay network breadcrumb that was skipped. */
- function buildSkippedNetworkRequestOrResponse(bodySize) {
- return {
- headers: {},
- size: bodySize,
- _meta: {
- warnings: ['URL_SKIPPED'],
- },
- };
- }
- /** Build the request or response part of a replay network breadcrumb. */
- function buildNetworkRequestOrResponse(
- headers,
- bodySize,
- body,
- ) {
- if (!bodySize && Object.keys(headers).length === 0) {
- return undefined;
- }
- if (!bodySize) {
- return {
- headers,
- };
- }
- if (!body) {
- return {
- headers,
- size: bodySize,
- };
- }
- const info = {
- headers,
- size: bodySize,
- };
- const { body: normalizedBody, warnings } = normalizeNetworkBody(body);
- info.body = normalizedBody;
- if (warnings && warnings.length > 0) {
- info._meta = {
- warnings,
- };
- }
- return info;
- }
- /** Filter a set of headers */
- function getAllowedHeaders(headers, allowedHeaders) {
- return Object.keys(headers).reduce((filteredHeaders, key) => {
- const normalizedKey = key.toLowerCase();
- // Avoid putting empty strings into the headers
- if (allowedHeaders.includes(normalizedKey) && headers[key]) {
- filteredHeaders[normalizedKey] = headers[key];
- }
- return filteredHeaders;
- }, {});
- }
- function _serializeFormData(formData) {
- // This is a bit simplified, but gives us a decent estimate
- // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13'
- // @ts-expect-error passing FormData to URLSearchParams actually works
- return new URLSearchParams(formData).toString();
- }
- function normalizeNetworkBody(body)
- {
- if (!body || typeof body !== 'string') {
- return {
- body,
- };
- }
- const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE;
- const isProbablyJson = _strIsProbablyJson(body);
- if (exceedsSizeLimit) {
- const truncatedBody = body.slice(0, NETWORK_BODY_MAX_SIZE);
- if (isProbablyJson) {
- return {
- body: truncatedBody,
- warnings: ['MAYBE_JSON_TRUNCATED'],
- };
- }
- return {
- body: `${truncatedBody}…`,
- warnings: ['TEXT_TRUNCATED'],
- };
- }
- if (isProbablyJson) {
- try {
- const jsonBody = JSON.parse(body);
- return {
- body: jsonBody,
- };
- } catch (e3) {
- // fall back to just send the body as string
- }
- }
- return {
- body,
- };
- }
- function _strIsProbablyJson(str) {
- const first = str[0];
- const last = str[str.length - 1];
- // Simple check: If this does not start & end with {} or [], it's not JSON
- return (first === '[' && last === ']') || (first === '{' && last === '}');
- }
- /** Match an URL against a list of strings/Regex. */
- function urlMatches(url, urls) {
- const fullUrl = getFullUrl(url);
- return utils.stringMatchesSomePattern(fullUrl, urls);
- }
- /** exported for tests */
- function getFullUrl(url, baseURI = WINDOW.document.baseURI) {
- // Short circuit for common cases:
- if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith(WINDOW.location.origin)) {
- return url;
- }
- const fixedUrl = new URL(url, baseURI);
- // If these do not match, we are not dealing with a relative URL, so just return it
- if (fixedUrl.origin !== new URL(baseURI).origin) {
- return url;
- }
- const fullUrl = fixedUrl.href;
- // Remove trailing slashes, if they don't match the original URL
- if (!url.endsWith('/') && fullUrl.endsWith('/')) {
- return fullUrl.slice(0, -1);
- }
- return fullUrl;
- }
- /**
- * Capture a fetch breadcrumb to a replay.
- * This adds additional data (where approriate).
- */
- async function captureFetchBreadcrumbToReplay(
- breadcrumb,
- hint,
- options
- ,
- ) {
- try {
- const data = await _prepareFetchData(breadcrumb, hint, options);
- // Create a replay performance entry from this breadcrumb
- const result = makeNetworkReplayBreadcrumb('resource.fetch', data);
- addNetworkBreadcrumb(options.replay, result);
- } catch (error) {
- DEBUG_BUILD && utils.logger.error('[Replay] Failed to capture fetch breadcrumb', error);
- }
- }
- /**
- * Enrich a breadcrumb with additional data.
- * This has to be sync & mutate the given breadcrumb,
- * as the breadcrumb is afterwards consumed by other handlers.
- */
- function enrichFetchBreadcrumb(
- breadcrumb,
- hint,
- options,
- ) {
- const { input, response } = hint;
- const body = input ? _getFetchRequestArgBody(input) : undefined;
- const reqSize = getBodySize(body, options.textEncoder);
- const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined;
- if (reqSize !== undefined) {
- breadcrumb.data.request_body_size = reqSize;
- }
- if (resSize !== undefined) {
- breadcrumb.data.response_body_size = resSize;
- }
- }
- async function _prepareFetchData(
- breadcrumb,
- hint,
- options
- ,
- ) {
- const now = Date.now();
- const { startTimestamp = now, endTimestamp = now } = hint;
- const {
- url,
- method,
- status_code: statusCode = 0,
- request_body_size: requestBodySize,
- response_body_size: responseBodySize,
- } = breadcrumb.data;
- const captureDetails =
- urlMatches(url, options.networkDetailAllowUrls) && !urlMatches(url, options.networkDetailDenyUrls);
- const request = captureDetails
- ? _getRequestInfo(options, hint.input, requestBodySize)
- : buildSkippedNetworkRequestOrResponse(requestBodySize);
- const response = await _getResponseInfo(captureDetails, options, hint.response, responseBodySize);
- return {
- startTimestamp,
- endTimestamp,
- url,
- method,
- statusCode,
- request,
- response,
- };
- }
- function _getRequestInfo(
- { networkCaptureBodies, networkRequestHeaders },
- input,
- requestBodySize,
- ) {
- const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {};
- if (!networkCaptureBodies) {
- return buildNetworkRequestOrResponse(headers, requestBodySize, undefined);
- }
- // We only want to transmit string or string-like bodies
- const requestBody = _getFetchRequestArgBody(input);
- const [bodyStr, warning] = getBodyString(requestBody);
- const data = buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr);
- if (warning) {
- return mergeWarning(data, warning);
- }
- return data;
- }
- /** Exported only for tests. */
- async function _getResponseInfo(
- captureDetails,
- {
- networkCaptureBodies,
- textEncoder,
- networkResponseHeaders,
- }
- ,
- response,
- responseBodySize,
- ) {
- if (!captureDetails && responseBodySize !== undefined) {
- return buildSkippedNetworkRequestOrResponse(responseBodySize);
- }
- const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {};
- if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) {
- return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
- }
- const [bodyText, warning] = await _parseFetchResponseBody(response);
- const result = getResponseData(bodyText, {
- networkCaptureBodies,
- textEncoder,
- responseBodySize,
- captureDetails,
- headers,
- });
- if (warning) {
- return mergeWarning(result, warning);
- }
- return result;
- }
- function getResponseData(
- bodyText,
- {
- networkCaptureBodies,
- textEncoder,
- responseBodySize,
- captureDetails,
- headers,
- }
- ,
- ) {
- try {
- const size =
- bodyText && bodyText.length && responseBodySize === undefined
- ? getBodySize(bodyText, textEncoder)
- : responseBodySize;
- if (!captureDetails) {
- return buildSkippedNetworkRequestOrResponse(size);
- }
- if (networkCaptureBodies) {
- return buildNetworkRequestOrResponse(headers, size, bodyText);
- }
- return buildNetworkRequestOrResponse(headers, size, undefined);
- } catch (error) {
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize response body', error);
- // fallback
- return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
- }
- }
- async function _parseFetchResponseBody(response) {
- const res = _tryCloneResponse(response);
- if (!res) {
- return [undefined, 'BODY_PARSE_ERROR'];
- }
- try {
- const text = await _tryGetResponseText(res);
- return [text];
- } catch (error) {
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to get text body from response', error);
- return [undefined, 'BODY_PARSE_ERROR'];
- }
- }
- function _getFetchRequestArgBody(fetchArgs = []) {
- // We only support getting the body from the fetch options
- if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') {
- return undefined;
- }
- return (fetchArgs[1] ).body;
- }
- function getAllHeaders(headers, allowedHeaders) {
- const allHeaders = {};
- allowedHeaders.forEach(header => {
- if (headers.get(header)) {
- allHeaders[header] = headers.get(header) ;
- }
- });
- return allHeaders;
- }
- function getRequestHeaders(fetchArgs, allowedHeaders) {
- if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') {
- return getHeadersFromOptions(fetchArgs[0] , allowedHeaders);
- }
- if (fetchArgs.length === 2) {
- return getHeadersFromOptions(fetchArgs[1] , allowedHeaders);
- }
- return {};
- }
- function getHeadersFromOptions(
- input,
- allowedHeaders,
- ) {
- if (!input) {
- return {};
- }
- const headers = input.headers;
- if (!headers) {
- return {};
- }
- if (headers instanceof Headers) {
- return getAllHeaders(headers, allowedHeaders);
- }
- // We do not support this, as it is not really documented (anymore?)
- if (Array.isArray(headers)) {
- return {};
- }
- return getAllowedHeaders(headers, allowedHeaders);
- }
- function _tryCloneResponse(response) {
- try {
- // We have to clone this, as the body can only be read once
- return response.clone();
- } catch (error) {
- // this can throw if the response was already consumed before
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to clone response body', error);
- }
- }
- /**
- * Get the response body of a fetch request, or timeout after 500ms.
- * Fetch can return a streaming body, that may not resolve (or not for a long time).
- * If that happens, we rather abort after a short time than keep waiting for this.
- */
- function _tryGetResponseText(response) {
- return new Promise((resolve, reject) => {
- const timeout = setTimeout(() => reject(new Error('Timeout while trying to read response body')), 500);
- _getResponseText(response)
- .then(
- txt => resolve(txt),
- reason => reject(reason),
- )
- .finally(() => clearTimeout(timeout));
- });
- }
- async function _getResponseText(response) {
- // Force this to be a promise, just to be safe
- // eslint-disable-next-line no-return-await
- return await response.text();
- }
- /**
- * Capture an XHR breadcrumb to a replay.
- * This adds additional data (where approriate).
- */
- async function captureXhrBreadcrumbToReplay(
- breadcrumb,
- hint,
- options,
- ) {
- try {
- const data = _prepareXhrData(breadcrumb, hint, options);
- // Create a replay performance entry from this breadcrumb
- const result = makeNetworkReplayBreadcrumb('resource.xhr', data);
- addNetworkBreadcrumb(options.replay, result);
- } catch (error) {
- DEBUG_BUILD && utils.logger.error('[Replay] Failed to capture xhr breadcrumb', error);
- }
- }
- /**
- * Enrich a breadcrumb with additional data.
- * This has to be sync & mutate the given breadcrumb,
- * as the breadcrumb is afterwards consumed by other handlers.
- */
- function enrichXhrBreadcrumb(
- breadcrumb,
- hint,
- options,
- ) {
- const { xhr, input } = hint;
- if (!xhr) {
- return;
- }
- const reqSize = getBodySize(input, options.textEncoder);
- const resSize = xhr.getResponseHeader('content-length')
- ? parseContentLengthHeader(xhr.getResponseHeader('content-length'))
- : _getBodySize(xhr.response, xhr.responseType, options.textEncoder);
- if (reqSize !== undefined) {
- breadcrumb.data.request_body_size = reqSize;
- }
- if (resSize !== undefined) {
- breadcrumb.data.response_body_size = resSize;
- }
- }
- function _prepareXhrData(
- breadcrumb,
- hint,
- options,
- ) {
- const now = Date.now();
- const { startTimestamp = now, endTimestamp = now, input, xhr } = hint;
- const {
- url,
- method,
- status_code: statusCode = 0,
- request_body_size: requestBodySize,
- response_body_size: responseBodySize,
- } = breadcrumb.data;
- if (!url) {
- return null;
- }
- if (!xhr || !urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
- const request = buildSkippedNetworkRequestOrResponse(requestBodySize);
- const response = buildSkippedNetworkRequestOrResponse(responseBodySize);
- return {
- startTimestamp,
- endTimestamp,
- url,
- method,
- statusCode,
- request,
- response,
- };
- }
- const xhrInfo = xhr[utils.SENTRY_XHR_DATA_KEY];
- const networkRequestHeaders = xhrInfo
- ? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders)
- : {};
- const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders);
- const [requestBody, requestWarning] = options.networkCaptureBodies ? getBodyString(input) : [undefined];
- const [responseBody, responseWarning] = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : [undefined];
- const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody);
- const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody);
- return {
- startTimestamp,
- endTimestamp,
- url,
- method,
- statusCode,
- request: requestWarning ? mergeWarning(request, requestWarning) : request,
- response: responseWarning ? mergeWarning(response, responseWarning) : response,
- };
- }
- function getResponseHeaders(xhr) {
- const headers = xhr.getAllResponseHeaders();
- if (!headers) {
- return {};
- }
- return headers.split('\r\n').reduce((acc, line) => {
- const [key, value] = line.split(': ');
- acc[key.toLowerCase()] = value;
- return acc;
- }, {});
- }
- function _getXhrResponseBody(xhr) {
- // We collect errors that happen, but only log them if we can't get any response body
- const errors = [];
- try {
- return [xhr.responseText];
- } catch (e) {
- errors.push(e);
- }
- // Try to manually parse the response body, if responseText fails
- try {
- return _parseXhrResponse(xhr.response, xhr.responseType);
- } catch (e) {
- errors.push(e);
- }
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to get xhr response body', ...errors);
- return [undefined];
- }
- /**
- * Get the string representation of the XHR response.
- * Based on MDN, these are the possible types of the response:
- * string
- * ArrayBuffer
- * Blob
- * Document
- * POJO
- *
- * Exported only for tests.
- */
- function _parseXhrResponse(
- body,
- responseType,
- ) {
- try {
- if (typeof body === 'string') {
- return [body];
- }
- if (body instanceof Document) {
- return [body.body.outerHTML];
- }
- if (responseType === 'json' && body && typeof body === 'object') {
- return [JSON.stringify(body)];
- }
- if (!body) {
- return [undefined];
- }
- } catch (e2) {
- DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize body', body);
- return [undefined, 'BODY_PARSE_ERROR'];
- }
- DEBUG_BUILD && utils.logger.info('[Replay] Skipping network body because of body type', body);
- return [undefined, 'UNPARSEABLE_BODY_TYPE'];
- }
- function _getBodySize(
- body,
- responseType,
- textEncoder,
- ) {
- try {
- const bodyStr = responseType === 'json' && body && typeof body === 'object' ? JSON.stringify(body) : body;
- return getBodySize(bodyStr, textEncoder);
- } catch (e3) {
- return undefined;
- }
- }
- /**
- * This method does two things:
- * - It enriches the regular XHR/fetch breadcrumbs with request/response size data
- * - It captures the XHR/fetch breadcrumbs to the replay
- * (enriching it with further data that is _not_ added to the regular breadcrumbs)
- */
- function handleNetworkBreadcrumbs(replay) {
- const client = core.getClient();
- try {
- const textEncoder = new TextEncoder();
- const {
- networkDetailAllowUrls,
- networkDetailDenyUrls,
- networkCaptureBodies,
- networkRequestHeaders,
- networkResponseHeaders,
- } = replay.getOptions();
- const options = {
- replay,
- textEncoder,
- networkDetailAllowUrls,
- networkDetailDenyUrls,
- networkCaptureBodies,
- networkRequestHeaders,
- networkResponseHeaders,
- };
- if (client && client.on) {
- client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint));
- } else {
- // Fallback behavior
- utils.addFetchInstrumentationHandler(handleFetchSpanListener(replay));
- utils.addXhrInstrumentationHandler(handleXhrSpanListener(replay));
- }
- } catch (e2) {
- // Do nothing
- }
- }
- /** just exported for tests */
- function beforeAddNetworkBreadcrumb(
- options,
- breadcrumb,
- hint,
- ) {
- if (!breadcrumb.data) {
- return;
- }
- try {
- if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) {
- // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
- // Because the hook runs synchronously, and the breadcrumb is afterwards passed on
- // So any async mutations to it will not be reflected in the final breadcrumb
- enrichXhrBreadcrumb(breadcrumb, hint, options);
- // This call should not reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- captureXhrBreadcrumbToReplay(breadcrumb, hint, options);
- }
- if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) {
- // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
- // Because the hook runs synchronously, and the breadcrumb is afterwards passed on
- // So any async mutations to it will not be reflected in the final breadcrumb
- enrichFetchBreadcrumb(breadcrumb, hint, options);
- // This call should not reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- captureFetchBreadcrumbToReplay(breadcrumb, hint, options);
- }
- } catch (e) {
- DEBUG_BUILD && utils.logger.warn('Error when enriching network breadcrumb');
- }
- }
- function _isXhrBreadcrumb(breadcrumb) {
- return breadcrumb.category === 'xhr';
- }
- function _isFetchBreadcrumb(breadcrumb) {
- return breadcrumb.category === 'fetch';
- }
- function _isXhrHint(hint) {
- return hint && hint.xhr;
- }
- function _isFetchHint(hint) {
- return hint && hint.response;
- }
- let _LAST_BREADCRUMB = null;
- function isBreadcrumbWithCategory(breadcrumb) {
- return !!breadcrumb.category;
- }
- const handleScopeListener =
- (replay) =>
- (scope) => {
- if (!replay.isEnabled()) {
- return;
- }
- const result = handleScope(scope);
- if (!result) {
- return;
- }
- addBreadcrumbEvent(replay, result);
- };
- /**
- * An event handler to handle scope changes.
- */
- function handleScope(scope) {
- // TODO (v8): Remove this guard. This was put in place because we introduced
- // Scope.getLastBreadcrumb mid-v7 which caused incompatibilities with older SDKs.
- // For now, we'll just return null if the method doesn't exist but we should eventually
- // get rid of this guard.
- const newBreadcrumb = scope.getLastBreadcrumb && scope.getLastBreadcrumb();
- // Listener can be called when breadcrumbs have not changed, so we store the
- // reference to the last crumb and only return a crumb if it has changed
- if (_LAST_BREADCRUMB === newBreadcrumb || !newBreadcrumb) {
- return null;
- }
- _LAST_BREADCRUMB = newBreadcrumb;
- if (
- !isBreadcrumbWithCategory(newBreadcrumb) ||
- ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) ||
- newBreadcrumb.category.startsWith('ui.')
- ) {
- return null;
- }
- if (newBreadcrumb.category === 'console') {
- return normalizeConsoleBreadcrumb(newBreadcrumb);
- }
- return createBreadcrumb(newBreadcrumb);
- }
- /** exported for tests only */
- function normalizeConsoleBreadcrumb(
- breadcrumb,
- ) {
- const args = breadcrumb.data && breadcrumb.data.arguments;
- if (!Array.isArray(args) || args.length === 0) {
- return createBreadcrumb(breadcrumb);
- }
- let isTruncated = false;
- // Avoid giant args captures
- const normalizedArgs = args.map(arg => {
- if (!arg) {
- return arg;
- }
- if (typeof arg === 'string') {
- if (arg.length > CONSOLE_ARG_MAX_SIZE) {
- isTruncated = true;
- return `${arg.slice(0, CONSOLE_ARG_MAX_SIZE)}…`;
- }
- return arg;
- }
- if (typeof arg === 'object') {
- try {
- const normalizedArg = utils.normalize(arg, 7);
- const stringified = JSON.stringify(normalizedArg);
- if (stringified.length > CONSOLE_ARG_MAX_SIZE) {
- isTruncated = true;
- // We use the pretty printed JSON string here as a base
- return `${JSON.stringify(normalizedArg, null, 2).slice(0, CONSOLE_ARG_MAX_SIZE)}…`;
- }
- return normalizedArg;
- } catch (e) {
- // fall back to default
- }
- }
- return arg;
- });
- return createBreadcrumb({
- ...breadcrumb,
- data: {
- ...breadcrumb.data,
- arguments: normalizedArgs,
- ...(isTruncated ? { _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] } } : {}),
- },
- });
- }
- /**
- * Add global listeners that cannot be removed.
- */
- function addGlobalListeners(replay) {
- // Listeners from core SDK //
- const scope = core.getCurrentScope();
- const client = core.getClient();
- scope.addScopeListener(handleScopeListener(replay));
- utils.addClickKeypressInstrumentationHandler(handleDomListener(replay));
- utils.addHistoryInstrumentationHandler(handleHistorySpanListener(replay));
- handleNetworkBreadcrumbs(replay);
- // Tag all (non replay) events that get sent to Sentry with the current
- // replay ID so that we can reference them later in the UI
- const eventProcessor = handleGlobalEventListener(replay, !hasHooks(client));
- if (client && client.addEventProcessor) {
- client.addEventProcessor(eventProcessor);
- } else {
- core.addEventProcessor(eventProcessor);
- }
- // If a custom client has no hooks yet, we continue to use the "old" implementation
- if (hasHooks(client)) {
- client.on('beforeSendEvent', handleBeforeSendEvent(replay));
- client.on('afterSendEvent', handleAfterSendEvent(replay));
- client.on('createDsc', (dsc) => {
- const replayId = replay.getSessionId();
- // We do not want to set the DSC when in buffer mode, as that means the replay has not been sent (yet)
- if (replayId && replay.isEnabled() && replay.recordingMode === 'session') {
- // Ensure to check that the session is still active - it could have expired in the meanwhile
- const isSessionActive = replay.checkAndHandleExpiredSession();
- if (isSessionActive) {
- dsc.replay_id = replayId;
- }
- }
- });
- client.on('startTransaction', transaction => {
- replay.lastTransaction = transaction;
- });
- // We may be missing the initial startTransaction due to timing issues,
- // so we capture it on finish again.
- client.on('finishTransaction', transaction => {
- replay.lastTransaction = transaction;
- });
- // We want to flush replay
- client.on('beforeSendFeedback', (feedbackEvent, options) => {
- const replayId = replay.getSessionId();
- if (options && options.includeReplay && replay.isEnabled() && replayId) {
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- replay.flush();
- if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) {
- feedbackEvent.contexts.feedback.replay_id = replayId;
- }
- }
- });
- }
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function hasHooks(client) {
- return !!(client && client.on);
- }
- /**
- * Create a "span" for the total amount of memory being used by JS objects
- * (including v8 internal objects).
- */
- async function addMemoryEntry(replay) {
- // window.performance.memory is a non-standard API and doesn't work on all browsers, so we try-catch this
- try {
- return Promise.all(
- createPerformanceSpans(replay, [
- // @ts-expect-error memory doesn't exist on type Performance as the API is non-standard (we check that it exists above)
- createMemoryEntry(WINDOW.performance.memory),
- ]),
- );
- } catch (error) {
- // Do nothing
- return [];
- }
- }
- function createMemoryEntry(memoryEntry) {
- const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = memoryEntry;
- // we don't want to use `getAbsoluteTime` because it adds the event time to the
- // time origin, so we get the current timestamp instead
- const time = Date.now() / 1000;
- return {
- type: 'memory',
- name: 'memory',
- start: time,
- end: time,
- data: {
- memory: {
- jsHeapSizeLimit,
- totalJSHeapSize,
- usedJSHeapSize,
- },
- },
- };
- }
- /**
- * Heavily simplified debounce function based on lodash.debounce.
- *
- * This function takes a callback function (@param fun) and delays its invocation
- * by @param wait milliseconds. Optionally, a maxWait can be specified in @param options,
- * which ensures that the callback is invoked at least once after the specified max. wait time.
- *
- * @param func the function whose invocation is to be debounced
- * @param wait the minimum time until the function is invoked after it was called once
- * @param options the options object, which can contain the `maxWait` property
- *
- * @returns the debounced version of the function, which needs to be called at least once to start the
- * debouncing process. Subsequent calls will reset the debouncing timer and, in case @paramfunc
- * was already invoked in the meantime, return @param func's return value.
- * The debounced function has two additional properties:
- * - `flush`: Invokes the debounced function immediately and returns its return value
- * - `cancel`: Cancels the debouncing process and resets the debouncing timer
- */
- function debounce(func, wait, options) {
- let callbackReturnValue;
- let timerId;
- let maxTimerId;
- const maxWait = options && options.maxWait ? Math.max(options.maxWait, wait) : 0;
- function invokeFunc() {
- cancelTimers();
- callbackReturnValue = func();
- return callbackReturnValue;
- }
- function cancelTimers() {
- timerId !== undefined && clearTimeout(timerId);
- maxTimerId !== undefined && clearTimeout(maxTimerId);
- timerId = maxTimerId = undefined;
- }
- function flush() {
- if (timerId !== undefined || maxTimerId !== undefined) {
- return invokeFunc();
- }
- return callbackReturnValue;
- }
- function debounced() {
- if (timerId) {
- clearTimeout(timerId);
- }
- timerId = setTimeout(invokeFunc, wait);
- if (maxWait && maxTimerId === undefined) {
- maxTimerId = setTimeout(invokeFunc, maxWait);
- }
- return callbackReturnValue;
- }
- debounced.cancel = cancelTimers;
- debounced.flush = flush;
- return debounced;
- }
- /**
- * Handler for recording events.
- *
- * Adds to event buffer, and has varying flushing behaviors if the event was a checkout.
- */
- function getHandleRecordingEmit(replay) {
- let hadFirstEvent = false;
- return (event, _isCheckout) => {
- // If this is false, it means session is expired, create and a new session and wait for checkout
- if (!replay.checkAndHandleExpiredSession()) {
- DEBUG_BUILD && utils.logger.warn('[Replay] Received replay event after session expired.');
- return;
- }
- // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
- // We also want to treat the first event as a checkout, so we handle this specifically here
- const isCheckout = _isCheckout || !hadFirstEvent;
- hadFirstEvent = true;
- if (replay.clickDetector) {
- updateClickDetectorForRecordingEvent(replay.clickDetector, event);
- }
- // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
- replay.addUpdate(() => {
- // The session is always started immediately on pageload/init, but for
- // error-only replays, it should reflect the most recent checkout
- // when an error occurs. Clear any state that happens before this current
- // checkout. This needs to happen before `addEvent()` which updates state
- // dependent on this reset.
- if (replay.recordingMode === 'buffer' && isCheckout) {
- replay.setInitialState();
- }
- // If the event is not added (e.g. due to being paused, disabled, or out of the max replay duration),
- // Skip all further steps
- if (!addEventSync(replay, event, isCheckout)) {
- // Return true to skip scheduling a debounced flush
- return true;
- }
- // Different behavior for full snapshots (type=2), ignore other event types
- // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
- if (!isCheckout) {
- return false;
- }
- // Additionally, create a meta event that will capture certain SDK settings.
- // In order to handle buffer mode, this needs to either be done when we
- // receive checkout events or at flush time.
- //
- // `isCheckout` is always true, but want to be explicit that it should
- // only be added for checkouts
- addSettingsEvent(replay, isCheckout);
- // If there is a previousSessionId after a full snapshot occurs, then
- // the replay session was started due to session expiration. The new session
- // is started before triggering a new checkout and contains the id
- // of the previous session. Do not immediately flush in this case
- // to avoid capturing only the checkout and instead the replay will
- // be captured if they perform any follow-up actions.
- if (replay.session && replay.session.previousSessionId) {
- return true;
- }
- // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
- // this should usually be the timestamp of the checkout event, but to be safe...
- if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
- const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
- if (earliestEvent) {
- logInfo(
- `[Replay] Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`,
- replay.getOptions()._experiments.traceInternals,
- );
- replay.session.started = earliestEvent;
- if (replay.getOptions().stickySession) {
- saveSession(replay.session);
- }
- }
- }
- if (replay.recordingMode === 'session') {
- // If the full snapshot is due to an initial load, we will not have
- // a previous session ID. In this case, we want to buffer events
- // for a set amount of time before flushing. This can help avoid
- // capturing replays of users that immediately close the window.
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- void replay.flush();
- }
- return true;
- });
- };
- }
- /**
- * Exported for tests
- */
- function createOptionsEvent(replay) {
- const options = replay.getOptions();
- return {
- type: EventType.Custom,
- timestamp: Date.now(),
- data: {
- tag: 'options',
- payload: {
- shouldRecordCanvas: replay.isRecordingCanvas(),
- sessionSampleRate: options.sessionSampleRate,
- errorSampleRate: options.errorSampleRate,
- useCompressionOption: options.useCompression,
- blockAllMedia: options.blockAllMedia,
- maskAllText: options.maskAllText,
- maskAllInputs: options.maskAllInputs,
- useCompression: replay.eventBuffer ? replay.eventBuffer.type === 'worker' : false,
- networkDetailHasUrls: options.networkDetailAllowUrls.length > 0,
- networkCaptureBodies: options.networkCaptureBodies,
- networkRequestHasHeaders: options.networkRequestHeaders.length > 0,
- networkResponseHasHeaders: options.networkResponseHeaders.length > 0,
- },
- },
- };
- }
- /**
- * Add a "meta" event that contains a simplified view on current configuration
- * options. This should only be included on the first segment of a recording.
- */
- function addSettingsEvent(replay, isCheckout) {
- // Only need to add this event when sending the first segment
- if (!isCheckout || !replay.session || replay.session.segmentId !== 0) {
- return;
- }
- addEventSync(replay, createOptionsEvent(replay), false);
- }
- /**
- * Create a replay envelope ready to be sent.
- * This includes both the replay event, as well as the recording data.
- */
- function createReplayEnvelope(
- replayEvent,
- recordingData,
- dsn,
- tunnel,
- ) {
- return utils.createEnvelope(
- utils.createEventEnvelopeHeaders(replayEvent, utils.getSdkMetadataForEnvelopeHeader(replayEvent), tunnel, dsn),
- [
- [{ type: 'replay_event' }, replayEvent],
- [
- {
- type: 'replay_recording',
- // If string then we need to encode to UTF8, otherwise will have
- // wrong size. TextEncoder has similar browser support to
- // MutationObserver, although it does not accept IE11.
- length:
- typeof recordingData === 'string' ? new TextEncoder().encode(recordingData).length : recordingData.length,
- },
- recordingData,
- ],
- ],
- );
- }
- /**
- * Prepare the recording data ready to be sent.
- */
- function prepareRecordingData({
- recordingData,
- headers,
- }
- ) {
- let payloadWithSequence;
- // XXX: newline is needed to separate sequence id from events
- const replayHeaders = `${JSON.stringify(headers)}
- `;
- if (typeof recordingData === 'string') {
- payloadWithSequence = `${replayHeaders}${recordingData}`;
- } else {
- const enc = new TextEncoder();
- // XXX: newline is needed to separate sequence id from events
- const sequence = enc.encode(replayHeaders);
- // Merge the two Uint8Arrays
- payloadWithSequence = new Uint8Array(sequence.length + recordingData.length);
- payloadWithSequence.set(sequence);
- payloadWithSequence.set(recordingData, sequence.length);
- }
- return payloadWithSequence;
- }
- /**
- * Prepare a replay event & enrich it with the SDK metadata.
- */
- async function prepareReplayEvent({
- client,
- scope,
- replayId: event_id,
- event,
- }
- ) {
- const integrations =
- typeof client._integrations === 'object' && client._integrations !== null && !Array.isArray(client._integrations)
- ? Object.keys(client._integrations)
- : undefined;
- const eventHint = { event_id, integrations };
- if (client.emit) {
- client.emit('preprocessEvent', event, eventHint);
- }
- const preparedEvent = (await core.prepareEvent(
- client.getOptions(),
- event,
- eventHint,
- scope,
- client,
- core.getIsolationScope(),
- )) ;
- // If e.g. a global event processor returned null
- if (!preparedEvent) {
- return null;
- }
- // This normally happens in browser client "_prepareEvent"
- // but since we do not use this private method from the client, but rather the plain import
- // we need to do this manually.
- preparedEvent.platform = preparedEvent.platform || 'javascript';
- // extract the SDK name because `client._prepareEvent` doesn't add it to the event
- const metadata = client.getSdkMetadata && client.getSdkMetadata();
- const { name, version } = (metadata && metadata.sdk) || {};
- preparedEvent.sdk = {
- ...preparedEvent.sdk,
- name: name || 'sentry.javascript.unknown',
- version: version || '0.0.0',
- };
- return preparedEvent;
- }
- /**
- * Send replay attachment using `fetch()`
- */
- async function sendReplayRequest({
- recordingData,
- replayId,
- segmentId: segment_id,
- eventContext,
- timestamp,
- session,
- }) {
- const preparedRecordingData = prepareRecordingData({
- recordingData,
- headers: {
- segment_id,
- },
- });
- const { urls, errorIds, traceIds, initialTimestamp } = eventContext;
- const client = core.getClient();
- const scope = core.getCurrentScope();
- const transport = client && client.getTransport();
- const dsn = client && client.getDsn();
- if (!client || !transport || !dsn || !session.sampled) {
- return;
- }
- const baseEvent = {
- type: REPLAY_EVENT_NAME,
- replay_start_timestamp: initialTimestamp / 1000,
- timestamp: timestamp / 1000,
- error_ids: errorIds,
- trace_ids: traceIds,
- urls,
- replay_id: replayId,
- segment_id,
- replay_type: session.sampled,
- };
- const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent });
- if (!replayEvent) {
- // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
- client.recordDroppedEvent('event_processor', 'replay', baseEvent);
- logInfo('An event processor returned `null`, will not send event.');
- return;
- }
- /*
- For reference, the fully built event looks something like this:
- {
- "type": "replay_event",
- "timestamp": 1670837008.634,
- "error_ids": [
- "errorId"
- ],
- "trace_ids": [
- "traceId"
- ],
- "urls": [
- "https://example.com"
- ],
- "replay_id": "eventId",
- "segment_id": 3,
- "replay_type": "error",
- "platform": "javascript",
- "event_id": "eventId",
- "environment": "production",
- "sdk": {
- "integrations": [
- "BrowserTracing",
- "Replay"
- ],
- "name": "sentry.javascript.browser",
- "version": "7.25.0"
- },
- "sdkProcessingMetadata": {},
- "contexts": {
- },
- }
- */
- // Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
- // sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
- // have temporarily added, etc. Even if we don't happen to be using it at some point in the future, let's not get rid
- // of this `delete`, lest we miss putting it back in the next time the property is in use.)
- delete replayEvent.sdkProcessingMetadata;
- const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel);
- let response;
- try {
- response = await transport.send(envelope);
- } catch (err) {
- const error = new Error(UNABLE_TO_SEND_REPLAY);
- try {
- // In case browsers don't allow this property to be writable
- // @ts-expect-error This needs lib es2022 and newer
- error.cause = err;
- } catch (e) {
- // nothing to do
- }
- throw error;
- }
- // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
- if (!response) {
- return response;
- }
- // If the status code is invalid, we want to immediately stop & not retry
- if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
- throw new TransportStatusCodeError(response.statusCode);
- }
- const rateLimits = utils.updateRateLimits({}, response);
- if (utils.isRateLimited(rateLimits, 'replay')) {
- throw new RateLimitError(rateLimits);
- }
- return response;
- }
- /**
- * This error indicates that the transport returned an invalid status code.
- */
- class TransportStatusCodeError extends Error {
- constructor(statusCode) {
- super(`Transport returned status code ${statusCode}`);
- }
- }
- /**
- * This error indicates that we hit a rate limit API error.
- */
- class RateLimitError extends Error {
- constructor(rateLimits) {
- super('Rate limit hit');
- this.rateLimits = rateLimits;
- }
- }
- /**
- * Finalize and send the current replay event to Sentry
- */
- async function sendReplay(
- replayData,
- retryConfig = {
- count: 0,
- interval: RETRY_BASE_INTERVAL,
- },
- ) {
- const { recordingData, options } = replayData;
- // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check)
- if (!recordingData.length) {
- return;
- }
- try {
- await sendReplayRequest(replayData);
- return true;
- } catch (err) {
- if (err instanceof TransportStatusCodeError || err instanceof RateLimitError) {
- throw err;
- }
- // Capture error for every failed replay
- core.setContext('Replays', {
- _retryCount: retryConfig.count,
- });
- if (DEBUG_BUILD && options._experiments && options._experiments.captureExceptions) {
- core.captureException(err);
- }
- // If an error happened here, it's likely that uploading the attachment
- // failed, we'll can retry with the same events payload
- if (retryConfig.count >= RETRY_MAX_COUNT) {
- const error = new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`);
- try {
- // In case browsers don't allow this property to be writable
- // @ts-expect-error This needs lib es2022 and newer
- error.cause = err;
- } catch (e) {
- // nothing to do
- }
- throw error;
- }
- // will retry in intervals of 5, 10, 30
- retryConfig.interval *= ++retryConfig.count;
- return new Promise((resolve, reject) => {
- setTimeout(async () => {
- try {
- await sendReplay(replayData, retryConfig);
- resolve(true);
- } catch (err) {
- reject(err);
- }
- }, retryConfig.interval);
- });
- }
- }
- const THROTTLED = '__THROTTLED';
- const SKIPPED = '__SKIPPED';
- /**
- * Create a throttled function off a given function.
- * When calling the throttled function, it will call the original function only
- * if it hasn't been called more than `maxCount` times in the last `durationSeconds`.
- *
- * Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`,
- * or else the return value of the original function.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function throttle(
- fn,
- maxCount,
- durationSeconds,
- ) {
- const counter = new Map();
- const _cleanup = (now) => {
- const threshold = now - durationSeconds;
- counter.forEach((_value, key) => {
- if (key < threshold) {
- counter.delete(key);
- }
- });
- };
- const _getTotalCount = () => {
- return [...counter.values()].reduce((a, b) => a + b, 0);
- };
- let isThrottled = false;
- return (...rest) => {
- // Date in second-precision, which we use as basis for the throttling
- const now = Math.floor(Date.now() / 1000);
- // First, make sure to delete any old entries
- _cleanup(now);
- // If already over limit, do nothing
- if (_getTotalCount() >= maxCount) {
- const wasThrottled = isThrottled;
- isThrottled = true;
- return wasThrottled ? SKIPPED : THROTTLED;
- }
- isThrottled = false;
- const count = counter.get(now) || 0;
- counter.set(now, count + 1);
- return fn(...rest);
- };
- }
- /* eslint-disable max-lines */ // TODO: We might want to split this file up
- /**
- * The main replay container class, which holds all the state and methods for recording and sending replays.
- */
- class ReplayContainer {
- /**
- * Recording can happen in one of three modes:
- * - session: Record the whole session, sending it continuously
- * - buffer: Always keep the last 60s of recording, requires:
- * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs
- * - or calling `flush()` to send the replay
- */
- /**
- * The current or last active transcation.
- * This is only available when performance is enabled.
- */
- /**
- * These are here so we can overwrite them in tests etc.
- * @hidden
- */
- /**
- * Options to pass to `rrweb.record()`
- */
- /**
- * Timestamp of the last user activity. This lives across sessions.
- */
- /**
- * Is the integration currently active?
- */
- /**
- * Paused is a state where:
- * - DOM Recording is not listening at all
- * - Nothing will be added to event buffer (e.g. core SDK events)
- */
- /**
- * Have we attached listeners to the core SDK?
- * Note we have to track this as there is no way to remove instrumentation handlers.
- */
- /**
- * Function to stop recording
- */
- /**
- * Internal use for canvas recording options
- */
- constructor({
- options,
- recordingOptions,
- }
- ) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);
- this.eventBuffer = null;
- this.performanceEntries = [];
- this.replayPerformanceEntries = [];
- this.recordingMode = 'session';
- this.timeouts = {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
- } ;
- this._lastActivity = Date.now();
- this._isEnabled = false;
- this._isPaused = false;
- this._hasInitializedCoreListeners = false;
- this._context = {
- errorIds: new Set(),
- traceIds: new Set(),
- urls: [],
- initialTimestamp: Date.now(),
- initialUrl: '',
- };
- this._recordingOptions = recordingOptions;
- this._options = options;
- this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, {
- maxWait: this._options.flushMaxDelay,
- });
- this._throttledAddEvent = throttle(
- (event, isCheckout) => addEvent(this, event, isCheckout),
- // Max 300 events...
- 300,
- // ... per 5s
- 5,
- );
- const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions();
- const slowClickConfig = slowClickTimeout
- ? {
- threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
- timeout: slowClickTimeout,
- scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
- ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
- }
- : undefined;
- if (slowClickConfig) {
- this.clickDetector = new ClickDetector(this, slowClickConfig);
- }
- }
- /** Get the event context. */
- getContext() {
- return this._context;
- }
- /** If recording is currently enabled. */
- isEnabled() {
- return this._isEnabled;
- }
- /** If recording is currently paused. */
- isPaused() {
- return this._isPaused;
- }
- /**
- * Determine if canvas recording is enabled
- */
- isRecordingCanvas() {
- return Boolean(this._canvas);
- }
- /** Get the replay integration options. */
- getOptions() {
- return this._options;
- }
- /**
- * Initializes the plugin based on sampling configuration. Should not be
- * called outside of constructor.
- */
- initializeSampling(previousSessionId) {
- const { errorSampleRate, sessionSampleRate } = this._options;
- // If neither sample rate is > 0, then do nothing - user will need to call one of
- // `start()` or `startBuffering` themselves.
- if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
- return;
- }
- // Otherwise if there is _any_ sample rate set, try to load an existing
- // session, or create a new one.
- this._initializeSessionForSampling(previousSessionId);
- if (!this.session) {
- // This should not happen, something wrong has occurred
- this._handleException(new Error('Unable to initialize and create session'));
- return;
- }
- if (this.session.sampled === false) {
- // This should only occur if `errorSampleRate` is 0 and was unsampled for
- // session-based replay. In this case there is nothing to do.
- return;
- }
- // If segmentId > 0, it means we've previously already captured this session
- // In this case, we still want to continue in `session` recording mode
- this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session';
- logInfoNextTick(
- `[Replay] Starting replay in ${this.recordingMode} mode`,
- this._options._experiments.traceInternals,
- );
- this._initializeRecording();
- }
- /**
- * Start a replay regardless of sampling rate. Calling this will always
- * create a new session. Will throw an error if replay is already in progress.
- *
- * Creates or loads a session, attaches listeners to varying events (DOM,
- * _performanceObserver, Recording, Sentry SDK, etc)
- */
- start() {
- if (this._isEnabled && this.recordingMode === 'session') {
- throw new Error('Replay recording is already in progress');
- }
- if (this._isEnabled && this.recordingMode === 'buffer') {
- throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
- }
- logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
- const session = loadOrCreateSession(
- {
- maxReplayDuration: this._options.maxReplayDuration,
- sessionIdleExpire: this.timeouts.sessionIdleExpire,
- traceInternals: this._options._experiments.traceInternals,
- },
- {
- stickySession: this._options.stickySession,
- // This is intentional: create a new session-based replay when calling `start()`
- sessionSampleRate: 1,
- allowBuffering: false,
- },
- );
- this.session = session;
- this._initializeRecording();
- }
- /**
- * Start replay buffering. Buffers until `flush()` is called or, if
- * `replaysOnErrorSampleRate` > 0, an error occurs.
- */
- startBuffering() {
- if (this._isEnabled) {
- throw new Error('Replay recording is already in progress');
- }
- logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
- const session = loadOrCreateSession(
- {
- sessionIdleExpire: this.timeouts.sessionIdleExpire,
- maxReplayDuration: this._options.maxReplayDuration,
- traceInternals: this._options._experiments.traceInternals,
- },
- {
- stickySession: this._options.stickySession,
- sessionSampleRate: 0,
- allowBuffering: true,
- },
- );
- this.session = session;
- this.recordingMode = 'buffer';
- this._initializeRecording();
- }
- /**
- * Start recording.
- *
- * Note that this will cause a new DOM checkout
- */
- startRecording() {
- try {
- const canvasOptions = this._canvas;
- this._stopRecording = record({
- ...this._recordingOptions,
- // When running in error sampling mode, we need to overwrite `checkoutEveryNms`
- // Without this, it would record forever, until an error happens, which we don't want
- // instead, we'll always keep the last 60 seconds of replay before an error happened
- ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }),
- emit: getHandleRecordingEmit(this),
- onMutation: this._onMutationHandler,
- ...(canvasOptions
- ? {
- recordCanvas: canvasOptions.recordCanvas,
- getCanvasManager: canvasOptions.getCanvasManager,
- sampling: canvasOptions.sampling,
- dataURLOptions: canvasOptions.dataURLOptions,
- }
- : {}),
- });
- } catch (err) {
- this._handleException(err);
- }
- }
- /**
- * Stops the recording, if it was running.
- *
- * Returns true if it was previously stopped, or is now stopped,
- * otherwise false.
- */
- stopRecording() {
- try {
- if (this._stopRecording) {
- this._stopRecording();
- this._stopRecording = undefined;
- }
- return true;
- } catch (err) {
- this._handleException(err);
- return false;
- }
- }
- /**
- * Currently, this needs to be manually called (e.g. for tests). Sentry SDK
- * does not support a teardown
- */
- async stop({ forceFlush = false, reason } = {}) {
- if (!this._isEnabled) {
- return;
- }
- // We can't move `_isEnabled` after awaiting a flush, otherwise we can
- // enter into an infinite loop when `stop()` is called while flushing.
- this._isEnabled = false;
- try {
- logInfo(
- `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`,
- this._options._experiments.traceInternals,
- );
- this._removeListeners();
- this.stopRecording();
- this._debouncedFlush.cancel();
- // See comment above re: `_isEnabled`, we "force" a flush, ignoring the
- // `_isEnabled` state of the plugin since it was disabled above.
- if (forceFlush) {
- await this._flush({ force: true });
- }
- // After flush, destroy event buffer
- this.eventBuffer && this.eventBuffer.destroy();
- this.eventBuffer = null;
- // Clear session from session storage, note this means if a new session
- // is started after, it will not have `previousSessionId`
- clearSession(this);
- } catch (err) {
- this._handleException(err);
- }
- }
- /**
- * Pause some replay functionality. See comments for `_isPaused`.
- * This differs from stop as this only stops DOM recording, it is
- * not as thorough of a shutdown as `stop()`.
- */
- pause() {
- if (this._isPaused) {
- return;
- }
- this._isPaused = true;
- this.stopRecording();
- logInfo('[Replay] Pausing replay', this._options._experiments.traceInternals);
- }
- /**
- * Resumes recording, see notes for `pause().
- *
- * Note that calling `startRecording()` here will cause a
- * new DOM checkout.`
- */
- resume() {
- if (!this._isPaused || !this._checkSession()) {
- return;
- }
- this._isPaused = false;
- this.startRecording();
- logInfo('[Replay] Resuming replay', this._options._experiments.traceInternals);
- }
- /**
- * If not in "session" recording mode, flush event buffer which will create a new replay.
- * Unless `continueRecording` is false, the replay will continue to record and
- * behave as a "session"-based replay.
- *
- * Otherwise, queue up a flush.
- */
- async sendBufferedReplayOrFlush({ continueRecording = true } = {}) {
- if (this.recordingMode === 'session') {
- return this.flushImmediate();
- }
- const activityTime = Date.now();
- logInfo('[Replay] Converting buffer to session', this._options._experiments.traceInternals);
- // Allow flush to complete before resuming as a session recording, otherwise
- // the checkout from `startRecording` may be included in the payload.
- // Prefer to keep the error replay as a separate (and smaller) segment
- // than the session replay.
- await this.flushImmediate();
- const hasStoppedRecording = this.stopRecording();
- if (!continueRecording || !hasStoppedRecording) {
- return;
- }
- // To avoid race conditions where this is called multiple times, we check here again that we are still buffering
- if ((this.recordingMode ) === 'session') {
- return;
- }
- // Re-start recording in session-mode
- this.recordingMode = 'session';
- // Once this session ends, we do not want to refresh it
- if (this.session) {
- this._updateUserActivity(activityTime);
- this._updateSessionActivity(activityTime);
- this._maybeSaveSession();
- }
- this.startRecording();
- }
- /**
- * We want to batch uploads of replay events. Save events only if
- * `<flushMinDelay>` milliseconds have elapsed since the last event
- * *OR* if `<flushMaxDelay>` milliseconds have elapsed.
- *
- * Accepts a callback to perform side-effects and returns true to stop batch
- * processing and hand back control to caller.
- */
- addUpdate(cb) {
- // We need to always run `cb` (e.g. in the case of `this.recordingMode == 'buffer'`)
- const cbResult = cb();
- // If this option is turned on then we will only want to call `flush`
- // explicitly
- if (this.recordingMode === 'buffer') {
- return;
- }
- // If callback is true, we do not want to continue with flushing -- the
- // caller will need to handle it.
- if (cbResult === true) {
- return;
- }
- // addUpdate is called quite frequently - use _debouncedFlush so that it
- // respects the flush delays and does not flush immediately
- this._debouncedFlush();
- }
- /**
- * Updates the user activity timestamp and resumes recording. This should be
- * called in an event handler for a user action that we consider as the user
- * being "active" (e.g. a mouse click).
- */
- triggerUserActivity() {
- this._updateUserActivity();
- // This case means that recording was once stopped due to inactivity.
- // Ensure that recording is resumed.
- if (!this._stopRecording) {
- // Create a new session, otherwise when the user action is flushed, it
- // will get rejected due to an expired session.
- if (!this._checkSession()) {
- return;
- }
- // Note: This will cause a new DOM checkout
- this.resume();
- return;
- }
- // Otherwise... recording was never suspended, continue as normalish
- this.checkAndHandleExpiredSession();
- this._updateSessionActivity();
- }
- /**
- * Updates the user activity timestamp *without* resuming
- * recording. Some user events (e.g. keydown) can be create
- * low-value replays that only contain the keypress as a
- * breadcrumb. Instead this would require other events to
- * create a new replay after a session has expired.
- */
- updateUserActivity() {
- this._updateUserActivity();
- this._updateSessionActivity();
- }
- /**
- * Only flush if `this.recordingMode === 'session'`
- */
- conditionalFlush() {
- if (this.recordingMode === 'buffer') {
- return Promise.resolve();
- }
- return this.flushImmediate();
- }
- /**
- * Flush using debounce flush
- */
- flush() {
- return this._debouncedFlush() ;
- }
- /**
- * Always flush via `_debouncedFlush` so that we do not have flushes triggered
- * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
- * cases of mulitple flushes happening closely together.
- */
- flushImmediate() {
- this._debouncedFlush();
- // `.flush` is provided by the debounced function, analogously to lodash.debounce
- return this._debouncedFlush.flush() ;
- }
- /**
- * Cancels queued up flushes.
- */
- cancelFlush() {
- this._debouncedFlush.cancel();
- }
- /** Get the current sesion (=replay) ID */
- getSessionId() {
- return this.session && this.session.id;
- }
- /**
- * Checks if recording should be stopped due to user inactivity. Otherwise
- * check if session is expired and create a new session if so. Triggers a new
- * full snapshot on new session.
- *
- * Returns true if session is not expired, false otherwise.
- * @hidden
- */
- checkAndHandleExpiredSession() {
- // Prevent starting a new session if the last user activity is older than
- // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
- // session+recording. This creates noisy replays that do not have much
- // content in them.
- if (
- this._lastActivity &&
- isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
- this.session &&
- this.session.sampled === 'session'
- ) {
- // Pause recording only for session-based replays. Otherwise, resuming
- // will create a new replay and will conflict with users who only choose
- // to record error-based replays only. (e.g. the resumed replay will not
- // contain a reference to an error)
- this.pause();
- return;
- }
- // --- There is recent user activity --- //
- // This will create a new session if expired, based on expiry length
- if (!this._checkSession()) {
- // Check session handles the refreshing itself
- return false;
- }
- return true;
- }
- /**
- * Capture some initial state that can change throughout the lifespan of the
- * replay. This is required because otherwise they would be captured at the
- * first flush.
- */
- setInitialState() {
- const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
- const url = `${WINDOW.location.origin}${urlPath}`;
- this.performanceEntries = [];
- this.replayPerformanceEntries = [];
- // Reset _context as well
- this._clearContext();
- this._context.initialUrl = url;
- this._context.initialTimestamp = Date.now();
- this._context.urls.push(url);
- }
- /**
- * Add a breadcrumb event, that may be throttled.
- * If it was throttled, we add a custom breadcrumb to indicate that.
- */
- throttledAddEvent(
- event,
- isCheckout,
- ) {
- const res = this._throttledAddEvent(event, isCheckout);
- // If this is THROTTLED, it means we have throttled the event for the first time
- // In this case, we want to add a breadcrumb indicating that something was skipped
- if (res === THROTTLED) {
- const breadcrumb = createBreadcrumb({
- category: 'replay.throttled',
- });
- this.addUpdate(() => {
- // Return `false` if the event _was_ added, as that means we schedule a flush
- return !addEventSync(this, {
- type: ReplayEventTypeCustom,
- timestamp: breadcrumb.timestamp || 0,
- data: {
- tag: 'breadcrumb',
- payload: breadcrumb,
- metric: true,
- },
- });
- });
- }
- return res;
- }
- /**
- * This will get the parametrized route name of the current page.
- * This is only available if performance is enabled, and if an instrumented router is used.
- */
- getCurrentRoute() {
- // eslint-disable-next-line deprecation/deprecation
- const lastTransaction = this.lastTransaction || core.getCurrentScope().getTransaction();
- const attributes = (lastTransaction && core.spanToJSON(lastTransaction).data) || {};
- const source = attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
- if (!lastTransaction || !source || !['route', 'custom'].includes(source)) {
- return undefined;
- }
- return core.spanToJSON(lastTransaction).description;
- }
- /**
- * Initialize and start all listeners to varying events (DOM,
- * Performance Observer, Recording, Sentry SDK, etc)
- */
- _initializeRecording() {
- this.setInitialState();
- // this method is generally called on page load or manually - in both cases
- // we should treat it as an activity
- this._updateSessionActivity();
- this.eventBuffer = createEventBuffer({
- useCompression: this._options.useCompression,
- workerUrl: this._options.workerUrl,
- });
- this._removeListeners();
- this._addListeners();
- // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
- this._isEnabled = true;
- this._isPaused = false;
- this.startRecording();
- }
- /** A wrapper to conditionally capture exceptions. */
- _handleException(error) {
- DEBUG_BUILD && utils.logger.error('[Replay]', error);
- if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) {
- core.captureException(error);
- }
- }
- /**
- * Loads (or refreshes) the current session.
- */
- _initializeSessionForSampling(previousSessionId) {
- // Whenever there is _any_ error sample rate, we always allow buffering
- // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
- const allowBuffering = this._options.errorSampleRate > 0;
- const session = loadOrCreateSession(
- {
- sessionIdleExpire: this.timeouts.sessionIdleExpire,
- maxReplayDuration: this._options.maxReplayDuration,
- traceInternals: this._options._experiments.traceInternals,
- previousSessionId,
- },
- {
- stickySession: this._options.stickySession,
- sessionSampleRate: this._options.sessionSampleRate,
- allowBuffering,
- },
- );
- this.session = session;
- }
- /**
- * Checks and potentially refreshes the current session.
- * Returns false if session is not recorded.
- */
- _checkSession() {
- // If there is no session yet, we do not want to refresh anything
- // This should generally not happen, but to be safe....
- if (!this.session) {
- return false;
- }
- const currentSession = this.session;
- if (
- shouldRefreshSession(currentSession, {
- sessionIdleExpire: this.timeouts.sessionIdleExpire,
- maxReplayDuration: this._options.maxReplayDuration,
- })
- ) {
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this._refreshSession(currentSession);
- return false;
- }
- return true;
- }
- /**
- * Refresh a session with a new one.
- * This stops the current session (without forcing a flush, as that would never work since we are expired),
- * and then does a new sampling based on the refreshed session.
- */
- async _refreshSession(session) {
- if (!this._isEnabled) {
- return;
- }
- await this.stop({ reason: 'refresh session' });
- this.initializeSampling(session.id);
- }
- /**
- * Adds listeners to record events for the replay
- */
- _addListeners() {
- try {
- WINDOW.document.addEventListener('visibilitychange', this._handleVisibilityChange);
- WINDOW.addEventListener('blur', this._handleWindowBlur);
- WINDOW.addEventListener('focus', this._handleWindowFocus);
- WINDOW.addEventListener('keydown', this._handleKeyboardEvent);
- if (this.clickDetector) {
- this.clickDetector.addListeners();
- }
- // There is no way to remove these listeners, so ensure they are only added once
- if (!this._hasInitializedCoreListeners) {
- addGlobalListeners(this);
- this._hasInitializedCoreListeners = true;
- }
- } catch (err) {
- this._handleException(err);
- }
- this._performanceCleanupCallback = setupPerformanceObserver(this);
- }
- /**
- * Cleans up listeners that were created in `_addListeners`
- */
- _removeListeners() {
- try {
- WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange);
- WINDOW.removeEventListener('blur', this._handleWindowBlur);
- WINDOW.removeEventListener('focus', this._handleWindowFocus);
- WINDOW.removeEventListener('keydown', this._handleKeyboardEvent);
- if (this.clickDetector) {
- this.clickDetector.removeListeners();
- }
- if (this._performanceCleanupCallback) {
- this._performanceCleanupCallback();
- }
- } catch (err) {
- this._handleException(err);
- }
- }
- /**
- * Handle when visibility of the page content changes. Opening a new tab will
- * cause the state to change to hidden because of content of current page will
- * be hidden. Likewise, moving a different window to cover the contents of the
- * page will also trigger a change to a hidden state.
- */
- __init() {this._handleVisibilityChange = () => {
- if (WINDOW.document.visibilityState === 'visible') {
- this._doChangeToForegroundTasks();
- } else {
- this._doChangeToBackgroundTasks();
- }
- };}
- /**
- * Handle when page is blurred
- */
- __init2() {this._handleWindowBlur = () => {
- const breadcrumb = createBreadcrumb({
- category: 'ui.blur',
- });
- // Do not count blur as a user action -- it's part of the process of them
- // leaving the page
- this._doChangeToBackgroundTasks(breadcrumb);
- };}
- /**
- * Handle when page is focused
- */
- __init3() {this._handleWindowFocus = () => {
- const breadcrumb = createBreadcrumb({
- category: 'ui.focus',
- });
- // Do not count focus as a user action -- instead wait until they focus and
- // interactive with page
- this._doChangeToForegroundTasks(breadcrumb);
- };}
- /** Ensure page remains active when a key is pressed. */
- __init4() {this._handleKeyboardEvent = (event) => {
- handleKeyboardEvent(this, event);
- };}
- /**
- * Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
- */
- _doChangeToBackgroundTasks(breadcrumb) {
- if (!this.session) {
- return;
- }
- const expired = isSessionExpired(this.session, {
- maxReplayDuration: this._options.maxReplayDuration,
- sessionIdleExpire: this.timeouts.sessionIdleExpire,
- });
- if (expired) {
- return;
- }
- if (breadcrumb) {
- this._createCustomBreadcrumb(breadcrumb);
- }
- // Send replay when the page/tab becomes hidden. There is no reason to send
- // replay if it becomes visible, since no actions we care about were done
- // while it was hidden
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- void this.conditionalFlush();
- }
- /**
- * Tasks to run when we consider a page to be visible (via focus and/or visibility)
- */
- _doChangeToForegroundTasks(breadcrumb) {
- if (!this.session) {
- return;
- }
- const isSessionActive = this.checkAndHandleExpiredSession();
- if (!isSessionActive) {
- // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
- // ms, we will re-use the existing session, otherwise create a new
- // session
- logInfo('[Replay] Document has become active, but session has expired');
- return;
- }
- if (breadcrumb) {
- this._createCustomBreadcrumb(breadcrumb);
- }
- }
- /**
- * Update user activity (across session lifespans)
- */
- _updateUserActivity(_lastActivity = Date.now()) {
- this._lastActivity = _lastActivity;
- }
- /**
- * Updates the session's last activity timestamp
- */
- _updateSessionActivity(_lastActivity = Date.now()) {
- if (this.session) {
- this.session.lastActivity = _lastActivity;
- this._maybeSaveSession();
- }
- }
- /**
- * Helper to create (and buffer) a replay breadcrumb from a core SDK breadcrumb
- */
- _createCustomBreadcrumb(breadcrumb) {
- this.addUpdate(() => {
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.throttledAddEvent({
- type: EventType.Custom,
- timestamp: breadcrumb.timestamp || 0,
- data: {
- tag: 'breadcrumb',
- payload: breadcrumb,
- },
- });
- });
- }
- /**
- * Observed performance events are added to `this.performanceEntries`. These
- * are included in the replay event before it is finished and sent to Sentry.
- */
- _addPerformanceEntries() {
- const performanceEntries = createPerformanceEntries(this.performanceEntries).concat(this.replayPerformanceEntries);
- this.performanceEntries = [];
- this.replayPerformanceEntries = [];
- return Promise.all(createPerformanceSpans(this, performanceEntries));
- }
- /**
- * Clear _context
- */
- _clearContext() {
- // XXX: `initialTimestamp` and `initialUrl` do not get cleared
- this._context.errorIds.clear();
- this._context.traceIds.clear();
- this._context.urls = [];
- }
- /** Update the initial timestamp based on the buffer content. */
- _updateInitialTimestampFromEventBuffer() {
- const { session, eventBuffer } = this;
- if (!session || !eventBuffer) {
- return;
- }
- // we only ever update this on the initial segment
- if (session.segmentId) {
- return;
- }
- const earliestEvent = eventBuffer.getEarliestTimestamp();
- if (earliestEvent && earliestEvent < this._context.initialTimestamp) {
- this._context.initialTimestamp = earliestEvent;
- }
- }
- /**
- * Return and clear _context
- */
- _popEventContext() {
- const _context = {
- initialTimestamp: this._context.initialTimestamp,
- initialUrl: this._context.initialUrl,
- errorIds: Array.from(this._context.errorIds),
- traceIds: Array.from(this._context.traceIds),
- urls: this._context.urls,
- };
- this._clearContext();
- return _context;
- }
- /**
- * Flushes replay event buffer to Sentry.
- *
- * Performance events are only added right before flushing - this is
- * due to the buffered performance observer events.
- *
- * Should never be called directly, only by `flush`
- */
- async _runFlush() {
- const replayId = this.getSessionId();
- if (!this.session || !this.eventBuffer || !replayId) {
- DEBUG_BUILD && utils.logger.error('[Replay] No session or eventBuffer found to flush.');
- return;
- }
- await this._addPerformanceEntries();
- // Check eventBuffer again, as it could have been stopped in the meanwhile
- if (!this.eventBuffer || !this.eventBuffer.hasEvents) {
- return;
- }
- // Only attach memory event if eventBuffer is not empty
- await addMemoryEntry(this);
- // Check eventBuffer again, as it could have been stopped in the meanwhile
- if (!this.eventBuffer) {
- return;
- }
- // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here
- if (replayId !== this.getSessionId()) {
- return;
- }
- try {
- // This uses the data from the eventBuffer, so we need to call this before `finish()
- this._updateInitialTimestampFromEventBuffer();
- const timestamp = Date.now();
- // Check total duration again, to avoid sending outdated stuff
- // We leave 30s wiggle room to accomodate late flushing etc.
- // This _could_ happen when the browser is suspended during flushing, in which case we just want to stop
- if (timestamp - this._context.initialTimestamp > this._options.maxReplayDuration + 30000) {
- throw new Error('Session is too long, not sending replay');
- }
- const eventContext = this._popEventContext();
- // Always increment segmentId regardless of outcome of sending replay
- const segmentId = this.session.segmentId++;
- this._maybeSaveSession();
- // Note this empties the event buffer regardless of outcome of sending replay
- const recordingData = await this.eventBuffer.finish();
- await sendReplay({
- replayId,
- recordingData,
- segmentId,
- eventContext,
- session: this.session,
- options: this.getOptions(),
- timestamp,
- });
- } catch (err) {
- this._handleException(err);
- // This means we retried 3 times and all of them failed,
- // or we ran into a problem we don't want to retry, like rate limiting.
- // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stop({ reason: 'sendReplay' });
- const client = core.getClient();
- if (client) {
- client.recordDroppedEvent('send_error', 'replay');
- }
- }
- }
- /**
- * Flush recording data to Sentry. Creates a lock so that only a single flush
- * can be active at a time. Do not call this directly.
- */
- __init5() {this._flush = async ({
- force = false,
- }
- = {}) => {
- if (!this._isEnabled && !force) {
- // This can happen if e.g. the replay was stopped because of exceeding the retry limit
- return;
- }
- if (!this.checkAndHandleExpiredSession()) {
- DEBUG_BUILD && utils.logger.error('[Replay] Attempting to finish replay event after session expired.');
- return;
- }
- if (!this.session) {
- // should never happen, as we would have bailed out before
- return;
- }
- const start = this.session.started;
- const now = Date.now();
- const duration = now - start;
- // A flush is about to happen, cancel any queued flushes
- this._debouncedFlush.cancel();
- // If session is too short, or too long (allow some wiggle room over maxReplayDuration), do not send it
- // This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar
- const tooShort = duration < this._options.minReplayDuration;
- const tooLong = duration > this._options.maxReplayDuration + 5000;
- if (tooShort || tooLong) {
- logInfo(
- `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${
- tooShort ? 'short' : 'long'
- }, not sending replay.`,
- this._options._experiments.traceInternals,
- );
- if (tooShort) {
- this._debouncedFlush();
- }
- return;
- }
- const eventBuffer = this.eventBuffer;
- if (eventBuffer && this.session.segmentId === 0 && !eventBuffer.hasCheckout) {
- logInfo('[Replay] Flushing initial segment without checkout.', this._options._experiments.traceInternals);
- // TODO FN: Evaluate if we want to stop here, or remove this again?
- }
- // this._flushLock acts as a lock so that future calls to `_flush()`
- // will be blocked until this promise resolves
- if (!this._flushLock) {
- this._flushLock = this._runFlush();
- await this._flushLock;
- this._flushLock = undefined;
- return;
- }
- // Wait for previous flush to finish, then call the debounced `_flush()`.
- // It's possible there are other flush requests queued and waiting for it
- // to resolve. We want to reduce all outstanding requests (as well as any
- // new flush requests that occur within a second of the locked flush
- // completing) into a single flush.
- try {
- await this._flushLock;
- } catch (err) {
- DEBUG_BUILD && utils.logger.error(err);
- } finally {
- this._debouncedFlush();
- }
- };}
- /** Save the session, if it is sticky */
- _maybeSaveSession() {
- if (this.session && this._options.stickySession) {
- saveSession(this.session);
- }
- }
- /** Handler for rrweb.record.onMutation */
- __init6() {this._onMutationHandler = (mutations) => {
- const count = mutations.length;
- const mutationLimit = this._options.mutationLimit;
- const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit;
- const overMutationLimit = mutationLimit && count > mutationLimit;
- // Create a breadcrumb if a lot of mutations happen at the same time
- // We can show this in the UI as an information with potential performance improvements
- if (count > mutationBreadcrumbLimit || overMutationLimit) {
- const breadcrumb = createBreadcrumb({
- category: 'replay.mutations',
- data: {
- count,
- limit: overMutationLimit,
- },
- });
- this._createCustomBreadcrumb(breadcrumb);
- }
- // Stop replay if over the mutation limit
- if (overMutationLimit) {
- // This should never reject
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' });
- return false;
- }
- // `true` means we use the regular mutation handling by rrweb
- return true;
- };}
- }
- function getOption(
- selectors,
- defaultSelectors,
- deprecatedClassOption,
- deprecatedSelectorOption,
- ) {
- const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : [];
- const allSelectors = [
- ...selectors,
- // @deprecated
- ...deprecatedSelectors,
- // sentry defaults
- ...defaultSelectors,
- ];
- // @deprecated
- if (typeof deprecatedClassOption !== 'undefined') {
- // NOTE: No support for RegExp
- if (typeof deprecatedClassOption === 'string') {
- allSelectors.push(`.${deprecatedClassOption}`);
- }
- utils.consoleSandbox(() => {
- // eslint-disable-next-line no-console
- console.warn(
- '[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.',
- );
- });
- }
- return allSelectors.join(',');
- }
- /**
- * Returns privacy related configuration for use in rrweb
- */
- function getPrivacyOptions({
- mask,
- unmask,
- block,
- unblock,
- ignore,
- // eslint-disable-next-line deprecation/deprecation
- blockClass,
- // eslint-disable-next-line deprecation/deprecation
- blockSelector,
- // eslint-disable-next-line deprecation/deprecation
- maskTextClass,
- // eslint-disable-next-line deprecation/deprecation
- maskTextSelector,
- // eslint-disable-next-line deprecation/deprecation
- ignoreClass,
- }) {
- const defaultBlockedElements = ['base[href="/"]'];
- const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector);
- const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']);
- const options = {
- // We are making the decision to make text and input selectors the same
- maskTextSelector: maskSelector,
- unmaskTextSelector: unmaskSelector,
- blockSelector: getOption(
- block,
- ['.sentry-block', '[data-sentry-block]', ...defaultBlockedElements],
- blockClass,
- blockSelector,
- ),
- unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']),
- ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]', 'input[type="file"]'], ignoreClass),
- };
- if (blockClass instanceof RegExp) {
- options.blockClass = blockClass;
- }
- if (maskTextClass instanceof RegExp) {
- options.maskTextClass = maskTextClass;
- }
- return options;
- }
- /**
- * Masks an attribute if necessary, otherwise return attribute value as-is.
- */
- function maskAttribute({
- el,
- key,
- maskAttributes,
- maskAllText,
- privacyOptions,
- value,
- }) {
- // We only mask attributes if `maskAllText` is true
- if (!maskAllText) {
- return value;
- }
- // unmaskTextSelector takes precendence
- if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) {
- return value;
- }
- if (
- maskAttributes.includes(key) ||
- // Need to mask `value` attribute for `<input>` if it's a button-like
- // type
- (key === 'value' && el.tagName === 'INPUT' && ['submit', 'button'].includes(el.getAttribute('type') || ''))
- ) {
- return value.replace(/[\S]/g, '*');
- }
- return value;
- }
- const MEDIA_SELECTORS =
- 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]';
- const DEFAULT_NETWORK_HEADERS = ['content-length', 'content-type', 'accept'];
- let _initialized = false;
- const replayIntegration = ((options) => {
- // eslint-disable-next-line deprecation/deprecation
- return new Replay(options);
- }) ;
- /**
- * The main replay integration class, to be passed to `init({ integrations: [] })`.
- * @deprecated Use `replayIntegration()` instead.
- */
- class Replay {
- /**
- * @inheritDoc
- */
- static __initStatic() {this.id = 'Replay';}
- /**
- * @inheritDoc
- */
- /**
- * Options to pass to `rrweb.record()`
- */
- /**
- * Initial options passed to the replay integration, merged with default values.
- * Note: `sessionSampleRate` and `errorSampleRate` are not required here, as they
- * can only be finally set when setupOnce() is called.
- *
- * @private
- */
- constructor({
- flushMinDelay = DEFAULT_FLUSH_MIN_DELAY,
- flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY,
- minReplayDuration = MIN_REPLAY_DURATION,
- maxReplayDuration = MAX_REPLAY_DURATION,
- stickySession = true,
- useCompression = true,
- workerUrl,
- _experiments = {},
- sessionSampleRate,
- errorSampleRate,
- maskAllText = true,
- maskAllInputs = true,
- blockAllMedia = true,
- mutationBreadcrumbLimit = 750,
- mutationLimit = 10000,
- slowClickTimeout = 7000,
- slowClickIgnoreSelectors = [],
- networkDetailAllowUrls = [],
- networkDetailDenyUrls = [],
- networkCaptureBodies = true,
- networkRequestHeaders = [],
- networkResponseHeaders = [],
- mask = [],
- maskAttributes = ['title', 'placeholder'],
- unmask = [],
- block = [],
- unblock = [],
- ignore = [],
- maskFn,
- beforeAddRecordingEvent,
- beforeErrorSampling,
- // eslint-disable-next-line deprecation/deprecation
- blockClass,
- // eslint-disable-next-line deprecation/deprecation
- blockSelector,
- // eslint-disable-next-line deprecation/deprecation
- maskInputOptions,
- // eslint-disable-next-line deprecation/deprecation
- maskTextClass,
- // eslint-disable-next-line deprecation/deprecation
- maskTextSelector,
- // eslint-disable-next-line deprecation/deprecation
- ignoreClass,
- } = {}) {
- // eslint-disable-next-line deprecation/deprecation
- this.name = Replay.id;
- const privacyOptions = getPrivacyOptions({
- mask,
- unmask,
- block,
- unblock,
- ignore,
- blockClass,
- blockSelector,
- maskTextClass,
- maskTextSelector,
- ignoreClass,
- });
- this._recordingOptions = {
- maskAllInputs,
- maskAllText,
- maskInputOptions: { ...(maskInputOptions || {}), password: true },
- maskTextFn: maskFn,
- maskInputFn: maskFn,
- maskAttributeFn: (key, value, el) =>
- maskAttribute({
- maskAttributes,
- maskAllText,
- privacyOptions,
- key,
- value,
- el,
- }),
- ...privacyOptions,
- // Our defaults
- slimDOMOptions: 'all',
- inlineStylesheet: true,
- // Disable inline images as it will increase segment/replay size
- inlineImages: false,
- // collect fonts, but be aware that `sentry.io` needs to be an allowed
- // origin for playback
- collectFonts: true,
- errorHandler: (err) => {
- try {
- err.__rrweb__ = true;
- } catch (error) {
- // ignore errors here
- // this can happen if the error is frozen or does not allow mutation for other reasons
- }
- },
- };
- this._initialOptions = {
- flushMinDelay,
- flushMaxDelay,
- minReplayDuration: Math.min(minReplayDuration, MIN_REPLAY_DURATION_LIMIT),
- maxReplayDuration: Math.min(maxReplayDuration, MAX_REPLAY_DURATION),
- stickySession,
- sessionSampleRate,
- errorSampleRate,
- useCompression,
- workerUrl,
- blockAllMedia,
- maskAllInputs,
- maskAllText,
- mutationBreadcrumbLimit,
- mutationLimit,
- slowClickTimeout,
- slowClickIgnoreSelectors,
- networkDetailAllowUrls,
- networkDetailDenyUrls,
- networkCaptureBodies,
- networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
- networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders),
- beforeAddRecordingEvent,
- beforeErrorSampling,
- _experiments,
- };
- if (typeof sessionSampleRate === 'number') {
- // eslint-disable-next-line
- console.warn(
- `[Replay] You are passing \`sessionSampleRate\` to the Replay integration.
- This option is deprecated and will be removed soon.
- Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options, e.g.:
- Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`,
- );
- this._initialOptions.sessionSampleRate = sessionSampleRate;
- }
- if (typeof errorSampleRate === 'number') {
- // eslint-disable-next-line
- console.warn(
- `[Replay] You are passing \`errorSampleRate\` to the Replay integration.
- This option is deprecated and will be removed soon.
- Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options, e.g.:
- Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
- );
- this._initialOptions.errorSampleRate = errorSampleRate;
- }
- if (this._initialOptions.blockAllMedia) {
- // `blockAllMedia` is a more user friendly option to configure blocking
- // embedded media elements
- this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector
- ? MEDIA_SELECTORS
- : `${this._recordingOptions.blockSelector},${MEDIA_SELECTORS}`;
- }
- if (this._isInitialized && utils.isBrowser()) {
- throw new Error('Multiple Sentry Session Replay instances are not supported');
- }
- this._isInitialized = true;
- }
- /** If replay has already been initialized */
- get _isInitialized() {
- return _initialized;
- }
- /** Update _isInitialized */
- set _isInitialized(value) {
- _initialized = value;
- }
- /**
- * Setup and initialize replay container
- */
- setupOnce() {
- if (!utils.isBrowser()) {
- return;
- }
- this._setup();
- // Once upon a time, we tried to create a transaction in `setupOnce` and it would
- // potentially create a transaction before some native SDK integrations have run
- // and applied their own global event processor. An example is:
- // https://github.com/getsentry/sentry-javascript/blob/b47ceafbdac7f8b99093ce6023726ad4687edc48/packages/browser/src/integrations/useragent.ts
- //
- // So we call `this._initialize()` in next event loop as a workaround to wait for other
- // global event processors to finish. This is no longer needed, but keeping it
- // here to avoid any future issues.
- setTimeout(() => this._initialize());
- }
- /**
- * Start a replay regardless of sampling rate. Calling this will always
- * create a new session. Will throw an error if replay is already in progress.
- *
- * Creates or loads a session, attaches listeners to varying events (DOM,
- * PerformanceObserver, Recording, Sentry SDK, etc)
- */
- start() {
- if (!this._replay) {
- return;
- }
- this._replay.start();
- }
- /**
- * Start replay buffering. Buffers until `flush()` is called or, if
- * `replaysOnErrorSampleRate` > 0, until an error occurs.
- */
- startBuffering() {
- if (!this._replay) {
- return;
- }
- this._replay.startBuffering();
- }
- /**
- * Currently, this needs to be manually called (e.g. for tests). Sentry SDK
- * does not support a teardown
- */
- stop() {
- if (!this._replay) {
- return Promise.resolve();
- }
- return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' });
- }
- /**
- * If not in "session" recording mode, flush event buffer which will create a new replay.
- * Unless `continueRecording` is false, the replay will continue to record and
- * behave as a "session"-based replay.
- *
- * Otherwise, queue up a flush.
- */
- flush(options) {
- if (!this._replay || !this._replay.isEnabled()) {
- return Promise.resolve();
- }
- return this._replay.sendBufferedReplayOrFlush(options);
- }
- /**
- * Get the current session ID.
- */
- getReplayId() {
- if (!this._replay || !this._replay.isEnabled()) {
- return;
- }
- return this._replay.getSessionId();
- }
- /**
- * Initializes replay.
- */
- _initialize() {
- if (!this._replay) {
- return;
- }
- // We have to run this in _initialize, because this runs in setTimeout
- // So when this runs all integrations have been added
- // Before this, we cannot access integrations on the client,
- // so we need to mutate the options here
- this._maybeLoadFromReplayCanvasIntegration();
- this._replay.initializeSampling();
- }
- /** Setup the integration. */
- _setup() {
- // Client is not available in constructor, so we need to wait until setupOnce
- const finalOptions = loadReplayOptionsFromClient(this._initialOptions);
- this._replay = new ReplayContainer({
- options: finalOptions,
- recordingOptions: this._recordingOptions,
- });
- }
- /** Get canvas options from ReplayCanvas integration, if it is also added. */
- _maybeLoadFromReplayCanvasIntegration() {
- // To save bundle size, we skip checking for stuff here
- // and instead just try-catch everything - as generally this should all be defined
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- try {
- const client = core.getClient();
- const canvasIntegration = client.getIntegrationByName('ReplayCanvas')
- ;
- if (!canvasIntegration) {
- return;
- }
- this._replay['_canvas'] = canvasIntegration.getOptions();
- } catch (e) {
- // ignore errors here
- }
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
- }
- }Replay.__initStatic();
- /** Parse Replay-related options from SDK options */
- function loadReplayOptionsFromClient(initialOptions) {
- const client = core.getClient();
- const opt = client && (client.getOptions() );
- const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...utils.dropUndefinedKeys(initialOptions) };
- if (!opt) {
- utils.consoleSandbox(() => {
- // eslint-disable-next-line no-console
- console.warn('SDK client is not available.');
- });
- return finalOptions;
- }
- if (
- initialOptions.sessionSampleRate == null && // TODO remove once deprecated rates are removed
- initialOptions.errorSampleRate == null && // TODO remove once deprecated rates are removed
- opt.replaysSessionSampleRate == null &&
- opt.replaysOnErrorSampleRate == null
- ) {
- utils.consoleSandbox(() => {
- // eslint-disable-next-line no-console
- console.warn(
- 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.',
- );
- });
- }
- if (typeof opt.replaysSessionSampleRate === 'number') {
- finalOptions.sessionSampleRate = opt.replaysSessionSampleRate;
- }
- if (typeof opt.replaysOnErrorSampleRate === 'number') {
- finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate;
- }
- return finalOptions;
- }
- function _getMergedNetworkHeaders(headers) {
- return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())];
- }
- /**
- * This is a small utility to get a type-safe instance of the Replay integration.
- */
- // eslint-disable-next-line deprecation/deprecation
- function getReplay() {
- const client = core.getClient();
- return (
- client && client.getIntegrationByName && client.getIntegrationByName('Replay')
- );
- }
- exports.Replay = Replay;
- exports.getReplay = getReplay;
- exports.replayIntegration = replayIntegration;
- //# sourceMappingURL=index.js.map
|