index.js 306 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547
  1. import { _nullishCoalesce, _optionalChain } from '@sentry/utils';
  2. import { addBreadcrumb, getClient, isSentryRequestUrl, getCurrentScope, addEventProcessor, prepareEvent, getIsolationScope, setContext, captureException, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
  3. import { GLOBAL_OBJ, normalize, fill, htmlTreeAsString, browserPerformanceTimeOrigin, logger, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addFetchInstrumentationHandler, addXhrInstrumentationHandler, addClickKeypressInstrumentationHandler, addHistoryInstrumentationHandler, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, updateRateLimits, isRateLimited, consoleSandbox, isBrowser } from '@sentry/utils';
  4. import { addPerformanceInstrumentationHandler, addLcpInstrumentationHandler } from '@sentry-internal/tracing';
  5. // exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
  6. // prevents the browser package from being bundled in the CDN bundle, and avoids a
  7. // circular dependency between the browser and replay packages should `@sentry/browser` import
  8. // from `@sentry/replay` in the future
  9. const WINDOW = GLOBAL_OBJ ;
  10. const REPLAY_SESSION_KEY = 'sentryReplaySession';
  11. const REPLAY_EVENT_NAME = 'replay_event';
  12. const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
  13. // The idle limit for a session after which recording is paused.
  14. const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms
  15. // The idle limit for a session after which the session expires.
  16. const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms
  17. /** Default flush delays */
  18. const DEFAULT_FLUSH_MIN_DELAY = 5000;
  19. // XXX: Temp fix for our debounce logic where `maxWait` would never occur if it
  20. // was the same as `wait`
  21. const DEFAULT_FLUSH_MAX_DELAY = 5500;
  22. /* How long to wait for error checkouts */
  23. const BUFFER_CHECKOUT_TIME = 60000;
  24. const RETRY_BASE_INTERVAL = 5000;
  25. const RETRY_MAX_COUNT = 3;
  26. /* The max (uncompressed) size in bytes of a network body. Any body larger than this will be truncated. */
  27. const NETWORK_BODY_MAX_SIZE = 150000;
  28. /* The max size of a single console arg that is captured. Any arg larger than this will be truncated. */
  29. const CONSOLE_ARG_MAX_SIZE = 5000;
  30. /* Min. time to wait before we consider something a slow click. */
  31. const SLOW_CLICK_THRESHOLD = 3000;
  32. /* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */
  33. const SLOW_CLICK_SCROLL_TIMEOUT = 300;
  34. /** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */
  35. const REPLAY_MAX_EVENT_BUFFER_SIZE = 20000000; // ~20MB
  36. /** Replays must be min. 5s long before we send them. */
  37. const MIN_REPLAY_DURATION = 4999;
  38. /* The max. allowed value that the minReplayDuration can be set to. */
  39. const MIN_REPLAY_DURATION_LIMIT = 15000;
  40. /** The max. length of a replay. */
  41. const MAX_REPLAY_DURATION = 3600000; // 60 minutes in ms;
  42. 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;
  43. (function (NodeType) {
  44. NodeType[NodeType["Document"] = 0] = "Document";
  45. NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
  46. NodeType[NodeType["Element"] = 2] = "Element";
  47. NodeType[NodeType["Text"] = 3] = "Text";
  48. NodeType[NodeType["CDATA"] = 4] = "CDATA";
  49. NodeType[NodeType["Comment"] = 5] = "Comment";
  50. })(NodeType$1 || (NodeType$1 = {}));
  51. function isElement$1(n) {
  52. return n.nodeType === n.ELEMENT_NODE;
  53. }
  54. function isShadowRoot(n) {
  55. const host = _optionalChain$5([n, 'optionalAccess', _ => _.host]);
  56. return Boolean(_optionalChain$5([host, 'optionalAccess', _2 => _2.shadowRoot]) === n);
  57. }
  58. function isNativeShadowDom(shadowRoot) {
  59. return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]';
  60. }
  61. function fixBrowserCompatibilityIssuesInCSS(cssText) {
  62. if (cssText.includes(' background-clip: text;') &&
  63. !cssText.includes(' -webkit-background-clip: text;')) {
  64. cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;');
  65. }
  66. return cssText;
  67. }
  68. function escapeImportStatement(rule) {
  69. const { cssText } = rule;
  70. if (cssText.split('"').length < 3)
  71. return cssText;
  72. const statement = ['@import', `url(${JSON.stringify(rule.href)})`];
  73. if (rule.layerName === '') {
  74. statement.push(`layer`);
  75. }
  76. else if (rule.layerName) {
  77. statement.push(`layer(${rule.layerName})`);
  78. }
  79. if (rule.supportsText) {
  80. statement.push(`supports(${rule.supportsText})`);
  81. }
  82. if (rule.media.length) {
  83. statement.push(rule.media.mediaText);
  84. }
  85. return statement.join(' ') + ';';
  86. }
  87. function stringifyStylesheet(s) {
  88. try {
  89. const rules = s.rules || s.cssRules;
  90. return rules
  91. ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join(''))
  92. : null;
  93. }
  94. catch (error) {
  95. return null;
  96. }
  97. }
  98. function stringifyRule(rule) {
  99. let importStringified;
  100. if (isCSSImportRule(rule)) {
  101. try {
  102. importStringified =
  103. stringifyStylesheet(rule.styleSheet) ||
  104. escapeImportStatement(rule);
  105. }
  106. catch (error) {
  107. }
  108. }
  109. else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
  110. return fixSafariColons(rule.cssText);
  111. }
  112. return importStringified || rule.cssText;
  113. }
  114. function fixSafariColons(cssStringified) {
  115. const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;
  116. return cssStringified.replace(regex, '$1\\$2');
  117. }
  118. function isCSSImportRule(rule) {
  119. return 'styleSheet' in rule;
  120. }
  121. function isCSSStyleRule(rule) {
  122. return 'selectorText' in rule;
  123. }
  124. class Mirror {
  125. constructor() {
  126. this.idNodeMap = new Map();
  127. this.nodeMetaMap = new WeakMap();
  128. }
  129. getId(n) {
  130. if (!n)
  131. return -1;
  132. const id = _optionalChain$5([this, 'access', _3 => _3.getMeta, 'call', _4 => _4(n), 'optionalAccess', _5 => _5.id]);
  133. return _nullishCoalesce$1(id, () => ( -1));
  134. }
  135. getNode(id) {
  136. return this.idNodeMap.get(id) || null;
  137. }
  138. getIds() {
  139. return Array.from(this.idNodeMap.keys());
  140. }
  141. getMeta(n) {
  142. return this.nodeMetaMap.get(n) || null;
  143. }
  144. removeNodeFromMap(n) {
  145. const id = this.getId(n);
  146. this.idNodeMap.delete(id);
  147. if (n.childNodes) {
  148. n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode));
  149. }
  150. }
  151. has(id) {
  152. return this.idNodeMap.has(id);
  153. }
  154. hasNode(node) {
  155. return this.nodeMetaMap.has(node);
  156. }
  157. add(n, meta) {
  158. const id = meta.id;
  159. this.idNodeMap.set(id, n);
  160. this.nodeMetaMap.set(n, meta);
  161. }
  162. replace(id, n) {
  163. const oldNode = this.getNode(id);
  164. if (oldNode) {
  165. const meta = this.nodeMetaMap.get(oldNode);
  166. if (meta)
  167. this.nodeMetaMap.set(n, meta);
  168. }
  169. this.idNodeMap.set(id, n);
  170. }
  171. reset() {
  172. this.idNodeMap = new Map();
  173. this.nodeMetaMap = new WeakMap();
  174. }
  175. }
  176. function createMirror() {
  177. return new Mirror();
  178. }
  179. function shouldMaskInput({ maskInputOptions, tagName, type, }) {
  180. if (tagName === 'OPTION') {
  181. tagName = 'SELECT';
  182. }
  183. return Boolean(maskInputOptions[tagName.toLowerCase()] ||
  184. (type && maskInputOptions[type]) ||
  185. type === 'password' ||
  186. (tagName === 'INPUT' && !type && maskInputOptions['text']));
  187. }
  188. function maskInputValue({ isMasked, element, value, maskInputFn, }) {
  189. let text = value || '';
  190. if (!isMasked) {
  191. return text;
  192. }
  193. if (maskInputFn) {
  194. text = maskInputFn(text, element);
  195. }
  196. return '*'.repeat(text.length);
  197. }
  198. function toLowerCase(str) {
  199. return str.toLowerCase();
  200. }
  201. function toUpperCase(str) {
  202. return str.toUpperCase();
  203. }
  204. const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__';
  205. function is2DCanvasBlank(canvas) {
  206. const ctx = canvas.getContext('2d');
  207. if (!ctx)
  208. return true;
  209. const chunkSize = 50;
  210. for (let x = 0; x < canvas.width; x += chunkSize) {
  211. for (let y = 0; y < canvas.height; y += chunkSize) {
  212. const getImageData = ctx.getImageData;
  213. const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData
  214. ? getImageData[ORIGINAL_ATTRIBUTE_NAME]
  215. : getImageData;
  216. const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer);
  217. if (pixelBuffer.some((pixel) => pixel !== 0))
  218. return false;
  219. }
  220. }
  221. return true;
  222. }
  223. function getInputType(element) {
  224. const type = element.type;
  225. return element.hasAttribute('data-rr-is-password')
  226. ? 'password'
  227. : type
  228. ?
  229. toLowerCase(type)
  230. : null;
  231. }
  232. function getInputValue(el, tagName, type) {
  233. if (tagName === 'INPUT' && (type === 'radio' || type === 'checkbox')) {
  234. return el.getAttribute('value') || '';
  235. }
  236. return el.value;
  237. }
  238. let _id = 1;
  239. const tagNameRegex = new RegExp('[^a-z0-9-_:]');
  240. const IGNORED_NODE = -2;
  241. function genId() {
  242. return _id++;
  243. }
  244. function getValidTagName(element) {
  245. if (element instanceof HTMLFormElement) {
  246. return 'form';
  247. }
  248. const processedTagName = toLowerCase(element.tagName);
  249. if (tagNameRegex.test(processedTagName)) {
  250. return 'div';
  251. }
  252. return processedTagName;
  253. }
  254. function extractOrigin(url) {
  255. let origin = '';
  256. if (url.indexOf('//') > -1) {
  257. origin = url.split('/').slice(0, 3).join('/');
  258. }
  259. else {
  260. origin = url.split('/')[0];
  261. }
  262. origin = origin.split('?')[0];
  263. return origin;
  264. }
  265. let canvasService;
  266. let canvasCtx;
  267. const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
  268. const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
  269. const URL_WWW_MATCH = /^www\..*/i;
  270. const DATA_URI = /^(data:)([^,]*),(.*)/i;
  271. function absoluteToStylesheet(cssText, href) {
  272. return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => {
  273. const filePath = path1 || path2 || path3;
  274. const maybeQuote = quote1 || quote2 || '';
  275. if (!filePath) {
  276. return origin;
  277. }
  278. if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {
  279. return `url(${maybeQuote}${filePath}${maybeQuote})`;
  280. }
  281. if (DATA_URI.test(filePath)) {
  282. return `url(${maybeQuote}${filePath}${maybeQuote})`;
  283. }
  284. if (filePath[0] === '/') {
  285. return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`;
  286. }
  287. const stack = href.split('/');
  288. const parts = filePath.split('/');
  289. stack.pop();
  290. for (const part of parts) {
  291. if (part === '.') {
  292. continue;
  293. }
  294. else if (part === '..') {
  295. stack.pop();
  296. }
  297. else {
  298. stack.push(part);
  299. }
  300. }
  301. return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;
  302. });
  303. }
  304. const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/;
  305. const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/;
  306. function getAbsoluteSrcsetString(doc, attributeValue) {
  307. if (attributeValue.trim() === '') {
  308. return attributeValue;
  309. }
  310. let pos = 0;
  311. function collectCharacters(regEx) {
  312. let chars;
  313. const match = regEx.exec(attributeValue.substring(pos));
  314. if (match) {
  315. chars = match[0];
  316. pos += chars.length;
  317. return chars;
  318. }
  319. return '';
  320. }
  321. const output = [];
  322. while (true) {
  323. collectCharacters(SRCSET_COMMAS_OR_SPACES);
  324. if (pos >= attributeValue.length) {
  325. break;
  326. }
  327. let url = collectCharacters(SRCSET_NOT_SPACES);
  328. if (url.slice(-1) === ',') {
  329. url = absoluteToDoc(doc, url.substring(0, url.length - 1));
  330. output.push(url);
  331. }
  332. else {
  333. let descriptorsStr = '';
  334. url = absoluteToDoc(doc, url);
  335. let inParens = false;
  336. while (true) {
  337. const c = attributeValue.charAt(pos);
  338. if (c === '') {
  339. output.push((url + descriptorsStr).trim());
  340. break;
  341. }
  342. else if (!inParens) {
  343. if (c === ',') {
  344. pos += 1;
  345. output.push((url + descriptorsStr).trim());
  346. break;
  347. }
  348. else if (c === '(') {
  349. inParens = true;
  350. }
  351. }
  352. else {
  353. if (c === ')') {
  354. inParens = false;
  355. }
  356. }
  357. descriptorsStr += c;
  358. pos += 1;
  359. }
  360. }
  361. }
  362. return output.join(', ');
  363. }
  364. function absoluteToDoc(doc, attributeValue) {
  365. if (!attributeValue || attributeValue.trim() === '') {
  366. return attributeValue;
  367. }
  368. const a = doc.createElement('a');
  369. a.href = attributeValue;
  370. return a.href;
  371. }
  372. function isSVGElement(el) {
  373. return Boolean(el.tagName === 'svg' || el.ownerSVGElement);
  374. }
  375. function getHref() {
  376. const a = document.createElement('a');
  377. a.href = '';
  378. return a.href;
  379. }
  380. function transformAttribute(doc, tagName, name, value, element, maskAttributeFn) {
  381. if (!value) {
  382. return value;
  383. }
  384. if (name === 'src' ||
  385. (name === 'href' && !(tagName === 'use' && value[0] === '#'))) {
  386. return absoluteToDoc(doc, value);
  387. }
  388. else if (name === 'xlink:href' && value[0] !== '#') {
  389. return absoluteToDoc(doc, value);
  390. }
  391. else if (name === 'background' &&
  392. (tagName === 'table' || tagName === 'td' || tagName === 'th')) {
  393. return absoluteToDoc(doc, value);
  394. }
  395. else if (name === 'srcset') {
  396. return getAbsoluteSrcsetString(doc, value);
  397. }
  398. else if (name === 'style') {
  399. return absoluteToStylesheet(value, getHref());
  400. }
  401. else if (tagName === 'object' && name === 'data') {
  402. return absoluteToDoc(doc, value);
  403. }
  404. if (typeof maskAttributeFn === 'function') {
  405. return maskAttributeFn(name, value, element);
  406. }
  407. return value;
  408. }
  409. function ignoreAttribute(tagName, name, _value) {
  410. return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';
  411. }
  412. function _isBlockedElement(element, blockClass, blockSelector, unblockSelector) {
  413. try {
  414. if (unblockSelector && element.matches(unblockSelector)) {
  415. return false;
  416. }
  417. if (typeof blockClass === 'string') {
  418. if (element.classList.contains(blockClass)) {
  419. return true;
  420. }
  421. }
  422. else {
  423. for (let eIndex = element.classList.length; eIndex--;) {
  424. const className = element.classList[eIndex];
  425. if (blockClass.test(className)) {
  426. return true;
  427. }
  428. }
  429. }
  430. if (blockSelector) {
  431. return element.matches(blockSelector);
  432. }
  433. }
  434. catch (e) {
  435. }
  436. return false;
  437. }
  438. function elementClassMatchesRegex(el, regex) {
  439. for (let eIndex = el.classList.length; eIndex--;) {
  440. const className = el.classList[eIndex];
  441. if (regex.test(className)) {
  442. return true;
  443. }
  444. }
  445. return false;
  446. }
  447. function distanceToMatch(node, matchPredicate, limit = Infinity, distance = 0) {
  448. if (!node)
  449. return -1;
  450. if (node.nodeType !== node.ELEMENT_NODE)
  451. return -1;
  452. if (distance > limit)
  453. return -1;
  454. if (matchPredicate(node))
  455. return distance;
  456. return distanceToMatch(node.parentNode, matchPredicate, limit, distance + 1);
  457. }
  458. function createMatchPredicate(className, selector) {
  459. return (node) => {
  460. const el = node;
  461. if (el === null)
  462. return false;
  463. try {
  464. if (className) {
  465. if (typeof className === 'string') {
  466. if (el.matches(`.${className}`))
  467. return true;
  468. }
  469. else if (elementClassMatchesRegex(el, className)) {
  470. return true;
  471. }
  472. }
  473. if (selector && el.matches(selector))
  474. return true;
  475. return false;
  476. }
  477. catch (e2) {
  478. return false;
  479. }
  480. };
  481. }
  482. function needMaskingText(node, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText) {
  483. try {
  484. const el = node.nodeType === node.ELEMENT_NODE
  485. ? node
  486. : node.parentElement;
  487. if (el === null)
  488. return false;
  489. if (el.tagName === 'INPUT') {
  490. const autocomplete = el.getAttribute('autocomplete');
  491. const disallowedAutocompleteValues = [
  492. 'current-password',
  493. 'new-password',
  494. 'cc-number',
  495. 'cc-exp',
  496. 'cc-exp-month',
  497. 'cc-exp-year',
  498. 'cc-csc',
  499. ];
  500. if (disallowedAutocompleteValues.includes(autocomplete)) {
  501. return true;
  502. }
  503. }
  504. let maskDistance = -1;
  505. let unmaskDistance = -1;
  506. if (maskAllText) {
  507. unmaskDistance = distanceToMatch(el, createMatchPredicate(unmaskTextClass, unmaskTextSelector));
  508. if (unmaskDistance < 0) {
  509. return true;
  510. }
  511. maskDistance = distanceToMatch(el, createMatchPredicate(maskTextClass, maskTextSelector), unmaskDistance >= 0 ? unmaskDistance : Infinity);
  512. }
  513. else {
  514. maskDistance = distanceToMatch(el, createMatchPredicate(maskTextClass, maskTextSelector));
  515. if (maskDistance < 0) {
  516. return false;
  517. }
  518. unmaskDistance = distanceToMatch(el, createMatchPredicate(unmaskTextClass, unmaskTextSelector), maskDistance >= 0 ? maskDistance : Infinity);
  519. }
  520. return maskDistance >= 0
  521. ? unmaskDistance >= 0
  522. ? maskDistance <= unmaskDistance
  523. : true
  524. : unmaskDistance >= 0
  525. ? false
  526. : !!maskAllText;
  527. }
  528. catch (e) {
  529. }
  530. return !!maskAllText;
  531. }
  532. function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) {
  533. const win = iframeEl.contentWindow;
  534. if (!win) {
  535. return;
  536. }
  537. let fired = false;
  538. let readyState;
  539. try {
  540. readyState = win.document.readyState;
  541. }
  542. catch (error) {
  543. return;
  544. }
  545. if (readyState !== 'complete') {
  546. const timer = setTimeout(() => {
  547. if (!fired) {
  548. listener();
  549. fired = true;
  550. }
  551. }, iframeLoadTimeout);
  552. iframeEl.addEventListener('load', () => {
  553. clearTimeout(timer);
  554. fired = true;
  555. listener();
  556. });
  557. return;
  558. }
  559. const blankUrl = 'about:blank';
  560. if (win.location.href !== blankUrl ||
  561. iframeEl.src === blankUrl ||
  562. iframeEl.src === '') {
  563. setTimeout(listener, 0);
  564. return iframeEl.addEventListener('load', listener);
  565. }
  566. iframeEl.addEventListener('load', listener);
  567. }
  568. function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) {
  569. let fired = false;
  570. let styleSheetLoaded;
  571. try {
  572. styleSheetLoaded = link.sheet;
  573. }
  574. catch (error) {
  575. return;
  576. }
  577. if (styleSheetLoaded)
  578. return;
  579. const timer = setTimeout(() => {
  580. if (!fired) {
  581. listener();
  582. fired = true;
  583. }
  584. }, styleSheetLoadTimeout);
  585. link.addEventListener('load', () => {
  586. clearTimeout(timer);
  587. fired = true;
  588. listener();
  589. });
  590. }
  591. function serializeNode(n, options) {
  592. const { doc, mirror, blockClass, blockSelector, unblockSelector, maskAllText, maskAttributeFn, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options;
  593. const rootId = getRootId(doc, mirror);
  594. switch (n.nodeType) {
  595. case n.DOCUMENT_NODE:
  596. if (n.compatMode !== 'CSS1Compat') {
  597. return {
  598. type: NodeType$1.Document,
  599. childNodes: [],
  600. compatMode: n.compatMode,
  601. };
  602. }
  603. else {
  604. return {
  605. type: NodeType$1.Document,
  606. childNodes: [],
  607. };
  608. }
  609. case n.DOCUMENT_TYPE_NODE:
  610. return {
  611. type: NodeType$1.DocumentType,
  612. name: n.name,
  613. publicId: n.publicId,
  614. systemId: n.systemId,
  615. rootId,
  616. };
  617. case n.ELEMENT_NODE:
  618. return serializeElementNode(n, {
  619. doc,
  620. blockClass,
  621. blockSelector,
  622. unblockSelector,
  623. inlineStylesheet,
  624. maskAttributeFn,
  625. maskInputOptions,
  626. maskInputFn,
  627. dataURLOptions,
  628. inlineImages,
  629. recordCanvas,
  630. keepIframeSrcFn,
  631. newlyAddedElement,
  632. rootId,
  633. maskAllText,
  634. maskTextClass,
  635. unmaskTextClass,
  636. maskTextSelector,
  637. unmaskTextSelector,
  638. });
  639. case n.TEXT_NODE:
  640. return serializeTextNode(n, {
  641. maskAllText,
  642. maskTextClass,
  643. unmaskTextClass,
  644. maskTextSelector,
  645. unmaskTextSelector,
  646. maskTextFn,
  647. maskInputOptions,
  648. maskInputFn,
  649. rootId,
  650. });
  651. case n.CDATA_SECTION_NODE:
  652. return {
  653. type: NodeType$1.CDATA,
  654. textContent: '',
  655. rootId,
  656. };
  657. case n.COMMENT_NODE:
  658. return {
  659. type: NodeType$1.Comment,
  660. textContent: n.textContent || '',
  661. rootId,
  662. };
  663. default:
  664. return false;
  665. }
  666. }
  667. function getRootId(doc, mirror) {
  668. if (!mirror.hasNode(doc))
  669. return undefined;
  670. const docId = mirror.getId(doc);
  671. return docId === 1 ? undefined : docId;
  672. }
  673. function serializeTextNode(n, options) {
  674. const { maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, maskTextFn, maskInputOptions, maskInputFn, rootId, } = options;
  675. const parentTagName = n.parentNode && n.parentNode.tagName;
  676. let textContent = n.textContent;
  677. const isStyle = parentTagName === 'STYLE' ? true : undefined;
  678. const isScript = parentTagName === 'SCRIPT' ? true : undefined;
  679. const isTextarea = parentTagName === 'TEXTAREA' ? true : undefined;
  680. if (isStyle && textContent) {
  681. try {
  682. if (n.nextSibling || n.previousSibling) {
  683. }
  684. else if (_optionalChain$5([n, 'access', _6 => _6.parentNode, 'access', _7 => _7.sheet, 'optionalAccess', _8 => _8.cssRules])) {
  685. textContent = stringifyStylesheet(n.parentNode.sheet);
  686. }
  687. }
  688. catch (err) {
  689. console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n);
  690. }
  691. textContent = absoluteToStylesheet(textContent, getHref());
  692. }
  693. if (isScript) {
  694. textContent = 'SCRIPT_PLACEHOLDER';
  695. }
  696. const forceMask = needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText);
  697. if (!isStyle && !isScript && !isTextarea && textContent && forceMask) {
  698. textContent = maskTextFn
  699. ? maskTextFn(textContent)
  700. : textContent.replace(/[\S]/g, '*');
  701. }
  702. if (isTextarea && textContent && (maskInputOptions.textarea || forceMask)) {
  703. textContent = maskInputFn
  704. ? maskInputFn(textContent, n.parentNode)
  705. : textContent.replace(/[\S]/g, '*');
  706. }
  707. if (parentTagName === 'OPTION' && textContent) {
  708. const isInputMasked = shouldMaskInput({
  709. type: null,
  710. tagName: parentTagName,
  711. maskInputOptions,
  712. });
  713. textContent = maskInputValue({
  714. isMasked: needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, isInputMasked),
  715. element: n,
  716. value: textContent,
  717. maskInputFn,
  718. });
  719. }
  720. return {
  721. type: NodeType$1.Text,
  722. textContent: textContent || '',
  723. isStyle,
  724. rootId,
  725. };
  726. }
  727. function serializeElementNode(n, options) {
  728. const { doc, blockClass, blockSelector, unblockSelector, inlineStylesheet, maskInputOptions = {}, maskAttributeFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, } = options;
  729. const needBlock = _isBlockedElement(n, blockClass, blockSelector, unblockSelector);
  730. const tagName = getValidTagName(n);
  731. let attributes = {};
  732. const len = n.attributes.length;
  733. for (let i = 0; i < len; i++) {
  734. const attr = n.attributes[i];
  735. if (attr.name && !ignoreAttribute(tagName, attr.name, attr.value)) {
  736. attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value, n, maskAttributeFn);
  737. }
  738. }
  739. if (tagName === 'link' && inlineStylesheet) {
  740. const stylesheet = Array.from(doc.styleSheets).find((s) => {
  741. return s.href === n.href;
  742. });
  743. let cssText = null;
  744. if (stylesheet) {
  745. cssText = stringifyStylesheet(stylesheet);
  746. }
  747. if (cssText) {
  748. delete attributes.rel;
  749. delete attributes.href;
  750. attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href);
  751. }
  752. }
  753. if (tagName === 'style' &&
  754. n.sheet &&
  755. !(n.innerText || n.textContent || '').trim().length) {
  756. const cssText = stringifyStylesheet(n.sheet);
  757. if (cssText) {
  758. attributes._cssText = absoluteToStylesheet(cssText, getHref());
  759. }
  760. }
  761. if (tagName === 'input' ||
  762. tagName === 'textarea' ||
  763. tagName === 'select' ||
  764. tagName === 'option') {
  765. const el = n;
  766. const type = getInputType(el);
  767. const value = getInputValue(el, toUpperCase(tagName), type);
  768. const checked = el.checked;
  769. if (type !== 'submit' && type !== 'button' && value) {
  770. const forceMask = needMaskingText(el, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, shouldMaskInput({
  771. type,
  772. tagName: toUpperCase(tagName),
  773. maskInputOptions,
  774. }));
  775. attributes.value = maskInputValue({
  776. isMasked: forceMask,
  777. element: el,
  778. value,
  779. maskInputFn,
  780. });
  781. }
  782. if (checked) {
  783. attributes.checked = checked;
  784. }
  785. }
  786. if (tagName === 'option') {
  787. if (n.selected && !maskInputOptions['select']) {
  788. attributes.selected = true;
  789. }
  790. else {
  791. delete attributes.selected;
  792. }
  793. }
  794. if (tagName === 'canvas' && recordCanvas) {
  795. if (n.__context === '2d') {
  796. if (!is2DCanvasBlank(n)) {
  797. attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality);
  798. }
  799. }
  800. else if (!('__context' in n)) {
  801. const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality);
  802. const blankCanvas = document.createElement('canvas');
  803. blankCanvas.width = n.width;
  804. blankCanvas.height = n.height;
  805. const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality);
  806. if (canvasDataURL !== blankCanvasDataURL) {
  807. attributes.rr_dataURL = canvasDataURL;
  808. }
  809. }
  810. }
  811. if (tagName === 'img' && inlineImages) {
  812. if (!canvasService) {
  813. canvasService = doc.createElement('canvas');
  814. canvasCtx = canvasService.getContext('2d');
  815. }
  816. const image = n;
  817. const oldValue = image.crossOrigin;
  818. image.crossOrigin = 'anonymous';
  819. const recordInlineImage = () => {
  820. image.removeEventListener('load', recordInlineImage);
  821. try {
  822. canvasService.width = image.naturalWidth;
  823. canvasService.height = image.naturalHeight;
  824. canvasCtx.drawImage(image, 0, 0);
  825. attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality);
  826. }
  827. catch (err) {
  828. console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`);
  829. }
  830. oldValue
  831. ? (attributes.crossOrigin = oldValue)
  832. : image.removeAttribute('crossorigin');
  833. };
  834. if (image.complete && image.naturalWidth !== 0)
  835. recordInlineImage();
  836. else
  837. image.addEventListener('load', recordInlineImage);
  838. }
  839. if (tagName === 'audio' || tagName === 'video') {
  840. attributes.rr_mediaState = n.paused
  841. ? 'paused'
  842. : 'played';
  843. attributes.rr_mediaCurrentTime = n.currentTime;
  844. }
  845. if (!newlyAddedElement) {
  846. if (n.scrollLeft) {
  847. attributes.rr_scrollLeft = n.scrollLeft;
  848. }
  849. if (n.scrollTop) {
  850. attributes.rr_scrollTop = n.scrollTop;
  851. }
  852. }
  853. if (needBlock) {
  854. const { width, height } = n.getBoundingClientRect();
  855. attributes = {
  856. class: attributes.class,
  857. rr_width: `${width}px`,
  858. rr_height: `${height}px`,
  859. };
  860. }
  861. if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) {
  862. if (!n.contentDocument) {
  863. attributes.rr_src = attributes.src;
  864. }
  865. delete attributes.src;
  866. }
  867. let isCustomElement;
  868. try {
  869. if (customElements.get(tagName))
  870. isCustomElement = true;
  871. }
  872. catch (e) {
  873. }
  874. return {
  875. type: NodeType$1.Element,
  876. tagName,
  877. attributes,
  878. childNodes: [],
  879. isSVG: isSVGElement(n) || undefined,
  880. needBlock,
  881. rootId,
  882. isCustom: isCustomElement,
  883. };
  884. }
  885. function lowerIfExists(maybeAttr) {
  886. if (maybeAttr === undefined || maybeAttr === null) {
  887. return '';
  888. }
  889. else {
  890. return maybeAttr.toLowerCase();
  891. }
  892. }
  893. function slimDOMExcluded(sn, slimDOMOptions) {
  894. if (slimDOMOptions.comment && sn.type === NodeType$1.Comment) {
  895. return true;
  896. }
  897. else if (sn.type === NodeType$1.Element) {
  898. if (slimDOMOptions.script &&
  899. (sn.tagName === 'script' ||
  900. (sn.tagName === 'link' &&
  901. (sn.attributes.rel === 'preload' ||
  902. sn.attributes.rel === 'modulepreload') &&
  903. sn.attributes.as === 'script') ||
  904. (sn.tagName === 'link' &&
  905. sn.attributes.rel === 'prefetch' &&
  906. typeof sn.attributes.href === 'string' &&
  907. sn.attributes.href.endsWith('.js')))) {
  908. return true;
  909. }
  910. else if (slimDOMOptions.headFavicon &&
  911. ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') ||
  912. (sn.tagName === 'meta' &&
  913. (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) ||
  914. lowerIfExists(sn.attributes.name) === 'application-name' ||
  915. lowerIfExists(sn.attributes.rel) === 'icon' ||
  916. lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' ||
  917. lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) {
  918. return true;
  919. }
  920. else if (sn.tagName === 'meta') {
  921. if (slimDOMOptions.headMetaDescKeywords &&
  922. lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) {
  923. return true;
  924. }
  925. else if (slimDOMOptions.headMetaSocial &&
  926. (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) ||
  927. lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) ||
  928. lowerIfExists(sn.attributes.name) === 'pinterest')) {
  929. return true;
  930. }
  931. else if (slimDOMOptions.headMetaRobots &&
  932. (lowerIfExists(sn.attributes.name) === 'robots' ||
  933. lowerIfExists(sn.attributes.name) === 'googlebot' ||
  934. lowerIfExists(sn.attributes.name) === 'bingbot')) {
  935. return true;
  936. }
  937. else if (slimDOMOptions.headMetaHttpEquiv &&
  938. sn.attributes['http-equiv'] !== undefined) {
  939. return true;
  940. }
  941. else if (slimDOMOptions.headMetaAuthorship &&
  942. (lowerIfExists(sn.attributes.name) === 'author' ||
  943. lowerIfExists(sn.attributes.name) === 'generator' ||
  944. lowerIfExists(sn.attributes.name) === 'framework' ||
  945. lowerIfExists(sn.attributes.name) === 'publisher' ||
  946. lowerIfExists(sn.attributes.name) === 'progid' ||
  947. lowerIfExists(sn.attributes.property).match(/^article:/) ||
  948. lowerIfExists(sn.attributes.property).match(/^product:/))) {
  949. return true;
  950. }
  951. else if (slimDOMOptions.headMetaVerification &&
  952. (lowerIfExists(sn.attributes.name) === 'google-site-verification' ||
  953. lowerIfExists(sn.attributes.name) === 'yandex-verification' ||
  954. lowerIfExists(sn.attributes.name) === 'csrf-token' ||
  955. lowerIfExists(sn.attributes.name) === 'p:domain_verify' ||
  956. lowerIfExists(sn.attributes.name) === 'verify-v1' ||
  957. lowerIfExists(sn.attributes.name) === 'verification' ||
  958. lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) {
  959. return true;
  960. }
  961. }
  962. }
  963. return false;
  964. }
  965. function serializeNodeWithId(n, options) {
  966. 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;
  967. let { preserveWhiteSpace = true } = options;
  968. const _serializedNode = serializeNode(n, {
  969. doc,
  970. mirror,
  971. blockClass,
  972. blockSelector,
  973. maskAllText,
  974. unblockSelector,
  975. maskTextClass,
  976. unmaskTextClass,
  977. maskTextSelector,
  978. unmaskTextSelector,
  979. inlineStylesheet,
  980. maskInputOptions,
  981. maskAttributeFn,
  982. maskTextFn,
  983. maskInputFn,
  984. dataURLOptions,
  985. inlineImages,
  986. recordCanvas,
  987. keepIframeSrcFn,
  988. newlyAddedElement,
  989. });
  990. if (!_serializedNode) {
  991. console.warn(n, 'not serialized');
  992. return null;
  993. }
  994. let id;
  995. if (mirror.hasNode(n)) {
  996. id = mirror.getId(n);
  997. }
  998. else if (slimDOMExcluded(_serializedNode, slimDOMOptions) ||
  999. (!preserveWhiteSpace &&
  1000. _serializedNode.type === NodeType$1.Text &&
  1001. !_serializedNode.isStyle &&
  1002. !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) {
  1003. id = IGNORED_NODE;
  1004. }
  1005. else {
  1006. id = genId();
  1007. }
  1008. const serializedNode = Object.assign(_serializedNode, { id });
  1009. mirror.add(n, serializedNode);
  1010. if (id === IGNORED_NODE) {
  1011. return null;
  1012. }
  1013. if (onSerialize) {
  1014. onSerialize(n);
  1015. }
  1016. let recordChild = !skipChild;
  1017. if (serializedNode.type === NodeType$1.Element) {
  1018. recordChild = recordChild && !serializedNode.needBlock;
  1019. delete serializedNode.needBlock;
  1020. const shadowRoot = n.shadowRoot;
  1021. if (shadowRoot && isNativeShadowDom(shadowRoot))
  1022. serializedNode.isShadowHost = true;
  1023. }
  1024. if ((serializedNode.type === NodeType$1.Document ||
  1025. serializedNode.type === NodeType$1.Element) &&
  1026. recordChild) {
  1027. if (slimDOMOptions.headWhitespace &&
  1028. serializedNode.type === NodeType$1.Element &&
  1029. serializedNode.tagName === 'head') {
  1030. preserveWhiteSpace = false;
  1031. }
  1032. const bypassOptions = {
  1033. doc,
  1034. mirror,
  1035. blockClass,
  1036. blockSelector,
  1037. maskAllText,
  1038. unblockSelector,
  1039. maskTextClass,
  1040. unmaskTextClass,
  1041. maskTextSelector,
  1042. unmaskTextSelector,
  1043. skipChild,
  1044. inlineStylesheet,
  1045. maskInputOptions,
  1046. maskAttributeFn,
  1047. maskTextFn,
  1048. maskInputFn,
  1049. slimDOMOptions,
  1050. dataURLOptions,
  1051. inlineImages,
  1052. recordCanvas,
  1053. preserveWhiteSpace,
  1054. onSerialize,
  1055. onIframeLoad,
  1056. iframeLoadTimeout,
  1057. onStylesheetLoad,
  1058. stylesheetLoadTimeout,
  1059. keepIframeSrcFn,
  1060. };
  1061. for (const childN of Array.from(n.childNodes)) {
  1062. const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
  1063. if (serializedChildNode) {
  1064. serializedNode.childNodes.push(serializedChildNode);
  1065. }
  1066. }
  1067. if (isElement$1(n) && n.shadowRoot) {
  1068. for (const childN of Array.from(n.shadowRoot.childNodes)) {
  1069. const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
  1070. if (serializedChildNode) {
  1071. isNativeShadowDom(n.shadowRoot) &&
  1072. (serializedChildNode.isShadow = true);
  1073. serializedNode.childNodes.push(serializedChildNode);
  1074. }
  1075. }
  1076. }
  1077. }
  1078. if (n.parentNode &&
  1079. isShadowRoot(n.parentNode) &&
  1080. isNativeShadowDom(n.parentNode)) {
  1081. serializedNode.isShadow = true;
  1082. }
  1083. if (serializedNode.type === NodeType$1.Element &&
  1084. serializedNode.tagName === 'iframe') {
  1085. onceIframeLoaded(n, () => {
  1086. const iframeDoc = n.contentDocument;
  1087. if (iframeDoc && onIframeLoad) {
  1088. const serializedIframeNode = serializeNodeWithId(iframeDoc, {
  1089. doc: iframeDoc,
  1090. mirror,
  1091. blockClass,
  1092. blockSelector,
  1093. unblockSelector,
  1094. maskAllText,
  1095. maskTextClass,
  1096. unmaskTextClass,
  1097. maskTextSelector,
  1098. unmaskTextSelector,
  1099. skipChild: false,
  1100. inlineStylesheet,
  1101. maskInputOptions,
  1102. maskAttributeFn,
  1103. maskTextFn,
  1104. maskInputFn,
  1105. slimDOMOptions,
  1106. dataURLOptions,
  1107. inlineImages,
  1108. recordCanvas,
  1109. preserveWhiteSpace,
  1110. onSerialize,
  1111. onIframeLoad,
  1112. iframeLoadTimeout,
  1113. onStylesheetLoad,
  1114. stylesheetLoadTimeout,
  1115. keepIframeSrcFn,
  1116. });
  1117. if (serializedIframeNode) {
  1118. onIframeLoad(n, serializedIframeNode);
  1119. }
  1120. }
  1121. }, iframeLoadTimeout);
  1122. }
  1123. if (serializedNode.type === NodeType$1.Element &&
  1124. serializedNode.tagName === 'link' &&
  1125. serializedNode.attributes.rel === 'stylesheet') {
  1126. onceStylesheetLoaded(n, () => {
  1127. if (onStylesheetLoad) {
  1128. const serializedLinkNode = serializeNodeWithId(n, {
  1129. doc,
  1130. mirror,
  1131. blockClass,
  1132. blockSelector,
  1133. unblockSelector,
  1134. maskAllText,
  1135. maskTextClass,
  1136. unmaskTextClass,
  1137. maskTextSelector,
  1138. unmaskTextSelector,
  1139. skipChild: false,
  1140. inlineStylesheet,
  1141. maskInputOptions,
  1142. maskAttributeFn,
  1143. maskTextFn,
  1144. maskInputFn,
  1145. slimDOMOptions,
  1146. dataURLOptions,
  1147. inlineImages,
  1148. recordCanvas,
  1149. preserveWhiteSpace,
  1150. onSerialize,
  1151. onIframeLoad,
  1152. iframeLoadTimeout,
  1153. onStylesheetLoad,
  1154. stylesheetLoadTimeout,
  1155. keepIframeSrcFn,
  1156. });
  1157. if (serializedLinkNode) {
  1158. onStylesheetLoad(n, serializedLinkNode);
  1159. }
  1160. }
  1161. }, stylesheetLoadTimeout);
  1162. }
  1163. return serializedNode;
  1164. }
  1165. function snapshot(n, options) {
  1166. 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 || {};
  1167. const maskInputOptions = maskAllInputs === true
  1168. ? {
  1169. color: true,
  1170. date: true,
  1171. 'datetime-local': true,
  1172. email: true,
  1173. month: true,
  1174. number: true,
  1175. range: true,
  1176. search: true,
  1177. tel: true,
  1178. text: true,
  1179. time: true,
  1180. url: true,
  1181. week: true,
  1182. textarea: true,
  1183. select: true,
  1184. }
  1185. : maskAllInputs === false
  1186. ? {}
  1187. : maskAllInputs;
  1188. const slimDOMOptions = slimDOM === true || slimDOM === 'all'
  1189. ?
  1190. {
  1191. script: true,
  1192. comment: true,
  1193. headFavicon: true,
  1194. headWhitespace: true,
  1195. headMetaDescKeywords: slimDOM === 'all',
  1196. headMetaSocial: true,
  1197. headMetaRobots: true,
  1198. headMetaHttpEquiv: true,
  1199. headMetaAuthorship: true,
  1200. headMetaVerification: true,
  1201. }
  1202. : slimDOM === false
  1203. ? {}
  1204. : slimDOM;
  1205. return serializeNodeWithId(n, {
  1206. doc: n,
  1207. mirror,
  1208. blockClass,
  1209. blockSelector,
  1210. unblockSelector,
  1211. maskAllText,
  1212. maskTextClass,
  1213. unmaskTextClass,
  1214. maskTextSelector,
  1215. unmaskTextSelector,
  1216. skipChild: false,
  1217. inlineStylesheet,
  1218. maskInputOptions,
  1219. maskAttributeFn,
  1220. maskTextFn,
  1221. maskInputFn,
  1222. slimDOMOptions,
  1223. dataURLOptions,
  1224. inlineImages,
  1225. recordCanvas,
  1226. preserveWhiteSpace,
  1227. onSerialize,
  1228. onIframeLoad,
  1229. iframeLoadTimeout,
  1230. onStylesheetLoad,
  1231. stylesheetLoadTimeout,
  1232. keepIframeSrcFn,
  1233. newlyAddedElement: false,
  1234. });
  1235. }
  1236. 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; }
  1237. function on(type, fn, target = document) {
  1238. const options = { capture: true, passive: true };
  1239. target.addEventListener(type, fn, options);
  1240. return () => target.removeEventListener(type, fn, options);
  1241. }
  1242. const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' +
  1243. '\r\n' +
  1244. 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' +
  1245. '\r\n' +
  1246. 'or you can use record.mirror to access the mirror instance during recording.';
  1247. let _mirror = {
  1248. map: {},
  1249. getId() {
  1250. console.error(DEPARTED_MIRROR_ACCESS_WARNING);
  1251. return -1;
  1252. },
  1253. getNode() {
  1254. console.error(DEPARTED_MIRROR_ACCESS_WARNING);
  1255. return null;
  1256. },
  1257. removeNodeFromMap() {
  1258. console.error(DEPARTED_MIRROR_ACCESS_WARNING);
  1259. },
  1260. has() {
  1261. console.error(DEPARTED_MIRROR_ACCESS_WARNING);
  1262. return false;
  1263. },
  1264. reset() {
  1265. console.error(DEPARTED_MIRROR_ACCESS_WARNING);
  1266. },
  1267. };
  1268. if (typeof window !== 'undefined' && window.Proxy && window.Reflect) {
  1269. _mirror = new Proxy(_mirror, {
  1270. get(target, prop, receiver) {
  1271. if (prop === 'map') {
  1272. console.error(DEPARTED_MIRROR_ACCESS_WARNING);
  1273. }
  1274. return Reflect.get(target, prop, receiver);
  1275. },
  1276. });
  1277. }
  1278. function throttle$1(func, wait, options = {}) {
  1279. let timeout = null;
  1280. let previous = 0;
  1281. return function (...args) {
  1282. const now = Date.now();
  1283. if (!previous && options.leading === false) {
  1284. previous = now;
  1285. }
  1286. const remaining = wait - (now - previous);
  1287. const context = this;
  1288. if (remaining <= 0 || remaining > wait) {
  1289. if (timeout) {
  1290. clearTimeout(timeout);
  1291. timeout = null;
  1292. }
  1293. previous = now;
  1294. func.apply(context, args);
  1295. }
  1296. else if (!timeout && options.trailing !== false) {
  1297. timeout = setTimeout(() => {
  1298. previous = options.leading === false ? 0 : Date.now();
  1299. timeout = null;
  1300. func.apply(context, args);
  1301. }, remaining);
  1302. }
  1303. };
  1304. }
  1305. function hookSetter(target, key, d, isRevoked, win = window) {
  1306. const original = win.Object.getOwnPropertyDescriptor(target, key);
  1307. win.Object.defineProperty(target, key, isRevoked
  1308. ? d
  1309. : {
  1310. set(value) {
  1311. setTimeout(() => {
  1312. d.set.call(this, value);
  1313. }, 0);
  1314. if (original && original.set) {
  1315. original.set.call(this, value);
  1316. }
  1317. },
  1318. });
  1319. return () => hookSetter(target, key, original || {}, true);
  1320. }
  1321. function patch(source, name, replacement) {
  1322. try {
  1323. if (!(name in source)) {
  1324. return () => {
  1325. };
  1326. }
  1327. const original = source[name];
  1328. const wrapped = replacement(original);
  1329. if (typeof wrapped === 'function') {
  1330. wrapped.prototype = wrapped.prototype || {};
  1331. Object.defineProperties(wrapped, {
  1332. __rrweb_original__: {
  1333. enumerable: false,
  1334. value: original,
  1335. },
  1336. });
  1337. }
  1338. source[name] = wrapped;
  1339. return () => {
  1340. source[name] = original;
  1341. };
  1342. }
  1343. catch (e2) {
  1344. return () => {
  1345. };
  1346. }
  1347. }
  1348. let nowTimestamp = Date.now;
  1349. if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) {
  1350. nowTimestamp = () => new Date().getTime();
  1351. }
  1352. function getWindowScroll(win) {
  1353. const doc = win.document;
  1354. return {
  1355. left: doc.scrollingElement
  1356. ? doc.scrollingElement.scrollLeft
  1357. : win.pageXOffset !== undefined
  1358. ? win.pageXOffset
  1359. : _optionalChain$4([doc, 'optionalAccess', _ => _.documentElement, 'access', _2 => _2.scrollLeft]) ||
  1360. _optionalChain$4([doc, 'optionalAccess', _3 => _3.body, 'optionalAccess', _4 => _4.parentElement, 'optionalAccess', _5 => _5.scrollLeft]) ||
  1361. _optionalChain$4([doc, 'optionalAccess', _6 => _6.body, 'optionalAccess', _7 => _7.scrollLeft]) ||
  1362. 0,
  1363. top: doc.scrollingElement
  1364. ? doc.scrollingElement.scrollTop
  1365. : win.pageYOffset !== undefined
  1366. ? win.pageYOffset
  1367. : _optionalChain$4([doc, 'optionalAccess', _8 => _8.documentElement, 'access', _9 => _9.scrollTop]) ||
  1368. _optionalChain$4([doc, 'optionalAccess', _10 => _10.body, 'optionalAccess', _11 => _11.parentElement, 'optionalAccess', _12 => _12.scrollTop]) ||
  1369. _optionalChain$4([doc, 'optionalAccess', _13 => _13.body, 'optionalAccess', _14 => _14.scrollTop]) ||
  1370. 0,
  1371. };
  1372. }
  1373. function getWindowHeight() {
  1374. return (window.innerHeight ||
  1375. (document.documentElement && document.documentElement.clientHeight) ||
  1376. (document.body && document.body.clientHeight));
  1377. }
  1378. function getWindowWidth() {
  1379. return (window.innerWidth ||
  1380. (document.documentElement && document.documentElement.clientWidth) ||
  1381. (document.body && document.body.clientWidth));
  1382. }
  1383. function isBlocked(node, blockClass, blockSelector, unblockSelector, checkAncestors) {
  1384. if (!node) {
  1385. return false;
  1386. }
  1387. const el = node.nodeType === node.ELEMENT_NODE
  1388. ? node
  1389. : node.parentElement;
  1390. if (!el)
  1391. return false;
  1392. const blockedPredicate = createMatchPredicate(blockClass, blockSelector);
  1393. if (!checkAncestors) {
  1394. const isUnblocked = unblockSelector && el.matches(unblockSelector);
  1395. return blockedPredicate(el) && !isUnblocked;
  1396. }
  1397. const blockDistance = distanceToMatch(el, blockedPredicate);
  1398. let unblockDistance = -1;
  1399. if (blockDistance < 0) {
  1400. return false;
  1401. }
  1402. if (unblockSelector) {
  1403. unblockDistance = distanceToMatch(el, createMatchPredicate(null, unblockSelector));
  1404. }
  1405. if (blockDistance > -1 && unblockDistance < 0) {
  1406. return true;
  1407. }
  1408. return blockDistance < unblockDistance;
  1409. }
  1410. function isSerialized(n, mirror) {
  1411. return mirror.getId(n) !== -1;
  1412. }
  1413. function isIgnored(n, mirror) {
  1414. return mirror.getId(n) === IGNORED_NODE;
  1415. }
  1416. function isAncestorRemoved(target, mirror) {
  1417. if (isShadowRoot(target)) {
  1418. return false;
  1419. }
  1420. const id = mirror.getId(target);
  1421. if (!mirror.has(id)) {
  1422. return true;
  1423. }
  1424. if (target.parentNode &&
  1425. target.parentNode.nodeType === target.DOCUMENT_NODE) {
  1426. return false;
  1427. }
  1428. if (!target.parentNode) {
  1429. return true;
  1430. }
  1431. return isAncestorRemoved(target.parentNode, mirror);
  1432. }
  1433. function legacy_isTouchEvent(event) {
  1434. return Boolean(event.changedTouches);
  1435. }
  1436. function polyfill(win = window) {
  1437. if ('NodeList' in win && !win.NodeList.prototype.forEach) {
  1438. win.NodeList.prototype.forEach = Array.prototype
  1439. .forEach;
  1440. }
  1441. if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) {
  1442. win.DOMTokenList.prototype.forEach = Array.prototype
  1443. .forEach;
  1444. }
  1445. if (!Node.prototype.contains) {
  1446. Node.prototype.contains = (...args) => {
  1447. let node = args[0];
  1448. if (!(0 in args)) {
  1449. throw new TypeError('1 argument is required');
  1450. }
  1451. do {
  1452. if (this === node) {
  1453. return true;
  1454. }
  1455. } while ((node = node && node.parentNode));
  1456. return false;
  1457. };
  1458. }
  1459. }
  1460. function isSerializedIframe(n, mirror) {
  1461. return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
  1462. }
  1463. function isSerializedStylesheet(n, mirror) {
  1464. return Boolean(n.nodeName === 'LINK' &&
  1465. n.nodeType === n.ELEMENT_NODE &&
  1466. n.getAttribute &&
  1467. n.getAttribute('rel') === 'stylesheet' &&
  1468. mirror.getMeta(n));
  1469. }
  1470. function hasShadowRoot(n) {
  1471. return Boolean(_optionalChain$4([n, 'optionalAccess', _18 => _18.shadowRoot]));
  1472. }
  1473. class StyleSheetMirror {
  1474. constructor() {
  1475. this.id = 1;
  1476. this.styleIDMap = new WeakMap();
  1477. this.idStyleMap = new Map();
  1478. }
  1479. getId(stylesheet) {
  1480. return _nullishCoalesce(this.styleIDMap.get(stylesheet), () => ( -1));
  1481. }
  1482. has(stylesheet) {
  1483. return this.styleIDMap.has(stylesheet);
  1484. }
  1485. add(stylesheet, id) {
  1486. if (this.has(stylesheet))
  1487. return this.getId(stylesheet);
  1488. let newId;
  1489. if (id === undefined) {
  1490. newId = this.id++;
  1491. }
  1492. else
  1493. newId = id;
  1494. this.styleIDMap.set(stylesheet, newId);
  1495. this.idStyleMap.set(newId, stylesheet);
  1496. return newId;
  1497. }
  1498. getStyle(id) {
  1499. return this.idStyleMap.get(id) || null;
  1500. }
  1501. reset() {
  1502. this.styleIDMap = new WeakMap();
  1503. this.idStyleMap = new Map();
  1504. this.id = 1;
  1505. }
  1506. generateId() {
  1507. return this.id++;
  1508. }
  1509. }
  1510. function getShadowHost(n) {
  1511. let shadowHost = null;
  1512. if (_optionalChain$4([n, 'access', _19 => _19.getRootNode, 'optionalCall', _20 => _20(), 'optionalAccess', _21 => _21.nodeType]) === Node.DOCUMENT_FRAGMENT_NODE &&
  1513. n.getRootNode().host)
  1514. shadowHost = n.getRootNode().host;
  1515. return shadowHost;
  1516. }
  1517. function getRootShadowHost(n) {
  1518. let rootShadowHost = n;
  1519. let shadowHost;
  1520. while ((shadowHost = getShadowHost(rootShadowHost)))
  1521. rootShadowHost = shadowHost;
  1522. return rootShadowHost;
  1523. }
  1524. function shadowHostInDom(n) {
  1525. const doc = n.ownerDocument;
  1526. if (!doc)
  1527. return false;
  1528. const shadowHost = getRootShadowHost(n);
  1529. return doc.contains(shadowHost);
  1530. }
  1531. function inDom(n) {
  1532. const doc = n.ownerDocument;
  1533. if (!doc)
  1534. return false;
  1535. return doc.contains(n) || shadowHostInDom(n);
  1536. }
  1537. let cachedRequestAnimationFrameImplementation;
  1538. function getRequestAnimationFrameImplementation() {
  1539. if (cachedRequestAnimationFrameImplementation) {
  1540. return cachedRequestAnimationFrameImplementation;
  1541. }
  1542. const document = window.document;
  1543. let requestAnimationFrameImplementation = window.requestAnimationFrame;
  1544. if (document && typeof document.createElement === 'function') {
  1545. try {
  1546. const sandbox = document.createElement('iframe');
  1547. sandbox.hidden = true;
  1548. document.head.appendChild(sandbox);
  1549. const contentWindow = sandbox.contentWindow;
  1550. if (contentWindow && contentWindow.requestAnimationFrame) {
  1551. requestAnimationFrameImplementation =
  1552. contentWindow.requestAnimationFrame;
  1553. }
  1554. document.head.removeChild(sandbox);
  1555. }
  1556. catch (e) {
  1557. }
  1558. }
  1559. return (cachedRequestAnimationFrameImplementation =
  1560. requestAnimationFrameImplementation.bind(window));
  1561. }
  1562. function onRequestAnimationFrame(...rest) {
  1563. return getRequestAnimationFrameImplementation()(...rest);
  1564. }
  1565. var EventType = /* @__PURE__ */ ((EventType2) => {
  1566. EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
  1567. EventType2[EventType2["Load"] = 1] = "Load";
  1568. EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
  1569. EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
  1570. EventType2[EventType2["Meta"] = 4] = "Meta";
  1571. EventType2[EventType2["Custom"] = 5] = "Custom";
  1572. EventType2[EventType2["Plugin"] = 6] = "Plugin";
  1573. return EventType2;
  1574. })(EventType || {});
  1575. var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
  1576. IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
  1577. IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
  1578. IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
  1579. IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
  1580. IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
  1581. IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
  1582. IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
  1583. IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
  1584. IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
  1585. IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
  1586. IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
  1587. IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
  1588. IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
  1589. IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
  1590. IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
  1591. IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
  1592. IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
  1593. return IncrementalSource2;
  1594. })(IncrementalSource || {});
  1595. var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => {
  1596. MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp";
  1597. MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown";
  1598. MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click";
  1599. MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu";
  1600. MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick";
  1601. MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus";
  1602. MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur";
  1603. MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart";
  1604. MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed";
  1605. MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd";
  1606. MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel";
  1607. return MouseInteractions2;
  1608. })(MouseInteractions || {});
  1609. var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => {
  1610. PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse";
  1611. PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen";
  1612. PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch";
  1613. return PointerTypes2;
  1614. })(PointerTypes || {});
  1615. 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; }
  1616. function isNodeInLinkedList(n) {
  1617. return '__ln' in n;
  1618. }
  1619. class DoubleLinkedList {
  1620. constructor() {
  1621. this.length = 0;
  1622. this.head = null;
  1623. this.tail = null;
  1624. }
  1625. get(position) {
  1626. if (position >= this.length) {
  1627. throw new Error('Position outside of list range');
  1628. }
  1629. let current = this.head;
  1630. for (let index = 0; index < position; index++) {
  1631. current = _optionalChain$3([current, 'optionalAccess', _ => _.next]) || null;
  1632. }
  1633. return current;
  1634. }
  1635. addNode(n) {
  1636. const node = {
  1637. value: n,
  1638. previous: null,
  1639. next: null,
  1640. };
  1641. n.__ln = node;
  1642. if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) {
  1643. const current = n.previousSibling.__ln.next;
  1644. node.next = current;
  1645. node.previous = n.previousSibling.__ln;
  1646. n.previousSibling.__ln.next = node;
  1647. if (current) {
  1648. current.previous = node;
  1649. }
  1650. }
  1651. else if (n.nextSibling &&
  1652. isNodeInLinkedList(n.nextSibling) &&
  1653. n.nextSibling.__ln.previous) {
  1654. const current = n.nextSibling.__ln.previous;
  1655. node.previous = current;
  1656. node.next = n.nextSibling.__ln;
  1657. n.nextSibling.__ln.previous = node;
  1658. if (current) {
  1659. current.next = node;
  1660. }
  1661. }
  1662. else {
  1663. if (this.head) {
  1664. this.head.previous = node;
  1665. }
  1666. node.next = this.head;
  1667. this.head = node;
  1668. }
  1669. if (node.next === null) {
  1670. this.tail = node;
  1671. }
  1672. this.length++;
  1673. }
  1674. removeNode(n) {
  1675. const current = n.__ln;
  1676. if (!this.head) {
  1677. return;
  1678. }
  1679. if (!current.previous) {
  1680. this.head = current.next;
  1681. if (this.head) {
  1682. this.head.previous = null;
  1683. }
  1684. else {
  1685. this.tail = null;
  1686. }
  1687. }
  1688. else {
  1689. current.previous.next = current.next;
  1690. if (current.next) {
  1691. current.next.previous = current.previous;
  1692. }
  1693. else {
  1694. this.tail = current.previous;
  1695. }
  1696. }
  1697. if (n.__ln) {
  1698. delete n.__ln;
  1699. }
  1700. this.length--;
  1701. }
  1702. }
  1703. const moveKey = (id, parentId) => `${id}@${parentId}`;
  1704. class MutationBuffer {
  1705. constructor() {
  1706. this.frozen = false;
  1707. this.locked = false;
  1708. this.texts = [];
  1709. this.attributes = [];
  1710. this.removes = [];
  1711. this.mapRemoves = [];
  1712. this.movedMap = {};
  1713. this.addedSet = new Set();
  1714. this.movedSet = new Set();
  1715. this.droppedSet = new Set();
  1716. this.processMutations = (mutations) => {
  1717. mutations.forEach(this.processMutation);
  1718. this.emit();
  1719. };
  1720. this.emit = () => {
  1721. if (this.frozen || this.locked) {
  1722. return;
  1723. }
  1724. const adds = [];
  1725. const addedIds = new Set();
  1726. const addList = new DoubleLinkedList();
  1727. const getNextId = (n) => {
  1728. let ns = n;
  1729. let nextId = IGNORED_NODE;
  1730. while (nextId === IGNORED_NODE) {
  1731. ns = ns && ns.nextSibling;
  1732. nextId = ns && this.mirror.getId(ns);
  1733. }
  1734. return nextId;
  1735. };
  1736. const pushAdd = (n) => {
  1737. if (!n.parentNode || !inDom(n)) {
  1738. return;
  1739. }
  1740. const parentId = isShadowRoot(n.parentNode)
  1741. ? this.mirror.getId(getShadowHost(n))
  1742. : this.mirror.getId(n.parentNode);
  1743. const nextId = getNextId(n);
  1744. if (parentId === -1 || nextId === -1) {
  1745. return addList.addNode(n);
  1746. }
  1747. const sn = serializeNodeWithId(n, {
  1748. doc: this.doc,
  1749. mirror: this.mirror,
  1750. blockClass: this.blockClass,
  1751. blockSelector: this.blockSelector,
  1752. maskAllText: this.maskAllText,
  1753. unblockSelector: this.unblockSelector,
  1754. maskTextClass: this.maskTextClass,
  1755. unmaskTextClass: this.unmaskTextClass,
  1756. maskTextSelector: this.maskTextSelector,
  1757. unmaskTextSelector: this.unmaskTextSelector,
  1758. skipChild: true,
  1759. newlyAddedElement: true,
  1760. inlineStylesheet: this.inlineStylesheet,
  1761. maskInputOptions: this.maskInputOptions,
  1762. maskAttributeFn: this.maskAttributeFn,
  1763. maskTextFn: this.maskTextFn,
  1764. maskInputFn: this.maskInputFn,
  1765. slimDOMOptions: this.slimDOMOptions,
  1766. dataURLOptions: this.dataURLOptions,
  1767. recordCanvas: this.recordCanvas,
  1768. inlineImages: this.inlineImages,
  1769. onSerialize: (currentN) => {
  1770. if (isSerializedIframe(currentN, this.mirror)) {
  1771. this.iframeManager.addIframe(currentN);
  1772. }
  1773. if (isSerializedStylesheet(currentN, this.mirror)) {
  1774. this.stylesheetManager.trackLinkElement(currentN);
  1775. }
  1776. if (hasShadowRoot(n)) {
  1777. this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc);
  1778. }
  1779. },
  1780. onIframeLoad: (iframe, childSn) => {
  1781. this.iframeManager.attachIframe(iframe, childSn);
  1782. this.shadowDomManager.observeAttachShadow(iframe);
  1783. },
  1784. onStylesheetLoad: (link, childSn) => {
  1785. this.stylesheetManager.attachLinkElement(link, childSn);
  1786. },
  1787. });
  1788. if (sn) {
  1789. adds.push({
  1790. parentId,
  1791. nextId,
  1792. node: sn,
  1793. });
  1794. addedIds.add(sn.id);
  1795. }
  1796. };
  1797. while (this.mapRemoves.length) {
  1798. this.mirror.removeNodeFromMap(this.mapRemoves.shift());
  1799. }
  1800. for (const n of this.movedSet) {
  1801. if (isParentRemoved(this.removes, n, this.mirror) &&
  1802. !this.movedSet.has(n.parentNode)) {
  1803. continue;
  1804. }
  1805. pushAdd(n);
  1806. }
  1807. for (const n of this.addedSet) {
  1808. if (!isAncestorInSet(this.droppedSet, n) &&
  1809. !isParentRemoved(this.removes, n, this.mirror)) {
  1810. pushAdd(n);
  1811. }
  1812. else if (isAncestorInSet(this.movedSet, n)) {
  1813. pushAdd(n);
  1814. }
  1815. else {
  1816. this.droppedSet.add(n);
  1817. }
  1818. }
  1819. let candidate = null;
  1820. while (addList.length) {
  1821. let node = null;
  1822. if (candidate) {
  1823. const parentId = this.mirror.getId(candidate.value.parentNode);
  1824. const nextId = getNextId(candidate.value);
  1825. if (parentId !== -1 && nextId !== -1) {
  1826. node = candidate;
  1827. }
  1828. }
  1829. if (!node) {
  1830. let tailNode = addList.tail;
  1831. while (tailNode) {
  1832. const _node = tailNode;
  1833. tailNode = tailNode.previous;
  1834. if (_node) {
  1835. const parentId = this.mirror.getId(_node.value.parentNode);
  1836. const nextId = getNextId(_node.value);
  1837. if (nextId === -1)
  1838. continue;
  1839. else if (parentId !== -1) {
  1840. node = _node;
  1841. break;
  1842. }
  1843. else {
  1844. const unhandledNode = _node.value;
  1845. if (unhandledNode.parentNode &&
  1846. unhandledNode.parentNode.nodeType ===
  1847. Node.DOCUMENT_FRAGMENT_NODE) {
  1848. const shadowHost = unhandledNode.parentNode
  1849. .host;
  1850. const parentId = this.mirror.getId(shadowHost);
  1851. if (parentId !== -1) {
  1852. node = _node;
  1853. break;
  1854. }
  1855. }
  1856. }
  1857. }
  1858. }
  1859. }
  1860. if (!node) {
  1861. while (addList.head) {
  1862. addList.removeNode(addList.head.value);
  1863. }
  1864. break;
  1865. }
  1866. candidate = node.previous;
  1867. addList.removeNode(node.value);
  1868. pushAdd(node.value);
  1869. }
  1870. const payload = {
  1871. texts: this.texts
  1872. .map((text) => ({
  1873. id: this.mirror.getId(text.node),
  1874. value: text.value,
  1875. }))
  1876. .filter((text) => !addedIds.has(text.id))
  1877. .filter((text) => this.mirror.has(text.id)),
  1878. attributes: this.attributes
  1879. .map((attribute) => {
  1880. const { attributes } = attribute;
  1881. if (typeof attributes.style === 'string') {
  1882. const diffAsStr = JSON.stringify(attribute.styleDiff);
  1883. const unchangedAsStr = JSON.stringify(attribute._unchangedStyles);
  1884. if (diffAsStr.length < attributes.style.length) {
  1885. if ((diffAsStr + unchangedAsStr).split('var(').length ===
  1886. attributes.style.split('var(').length) {
  1887. attributes.style = attribute.styleDiff;
  1888. }
  1889. }
  1890. }
  1891. return {
  1892. id: this.mirror.getId(attribute.node),
  1893. attributes: attributes,
  1894. };
  1895. })
  1896. .filter((attribute) => !addedIds.has(attribute.id))
  1897. .filter((attribute) => this.mirror.has(attribute.id)),
  1898. removes: this.removes,
  1899. adds,
  1900. };
  1901. if (!payload.texts.length &&
  1902. !payload.attributes.length &&
  1903. !payload.removes.length &&
  1904. !payload.adds.length) {
  1905. return;
  1906. }
  1907. this.texts = [];
  1908. this.attributes = [];
  1909. this.removes = [];
  1910. this.addedSet = new Set();
  1911. this.movedSet = new Set();
  1912. this.droppedSet = new Set();
  1913. this.movedMap = {};
  1914. this.mutationCb(payload);
  1915. };
  1916. this.processMutation = (m) => {
  1917. if (isIgnored(m.target, this.mirror)) {
  1918. return;
  1919. }
  1920. let unattachedDoc;
  1921. try {
  1922. unattachedDoc = document.implementation.createHTMLDocument();
  1923. }
  1924. catch (e) {
  1925. unattachedDoc = this.doc;
  1926. }
  1927. switch (m.type) {
  1928. case 'characterData': {
  1929. const value = m.target.textContent;
  1930. if (!isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) &&
  1931. value !== m.oldValue) {
  1932. this.texts.push({
  1933. value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextClass, this.unmaskTextSelector, this.maskAllText) && value
  1934. ? this.maskTextFn
  1935. ? this.maskTextFn(value)
  1936. : value.replace(/[\S]/g, '*')
  1937. : value,
  1938. node: m.target,
  1939. });
  1940. }
  1941. break;
  1942. }
  1943. case 'attributes': {
  1944. const target = m.target;
  1945. let attributeName = m.attributeName;
  1946. let value = m.target.getAttribute(attributeName);
  1947. if (attributeName === 'value') {
  1948. const type = getInputType(target);
  1949. const tagName = target.tagName;
  1950. value = getInputValue(target, tagName, type);
  1951. const isInputMasked = shouldMaskInput({
  1952. maskInputOptions: this.maskInputOptions,
  1953. tagName,
  1954. type,
  1955. });
  1956. const forceMask = needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextClass, this.unmaskTextSelector, isInputMasked);
  1957. value = maskInputValue({
  1958. isMasked: forceMask,
  1959. element: target,
  1960. value,
  1961. maskInputFn: this.maskInputFn,
  1962. });
  1963. }
  1964. if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) ||
  1965. value === m.oldValue) {
  1966. return;
  1967. }
  1968. let item = this.attributes.find((a) => a.node === m.target);
  1969. if (target.tagName === 'IFRAME' &&
  1970. attributeName === 'src' &&
  1971. !this.keepIframeSrcFn(value)) {
  1972. if (!target.contentDocument) {
  1973. attributeName = 'rr_src';
  1974. }
  1975. else {
  1976. return;
  1977. }
  1978. }
  1979. if (!item) {
  1980. item = {
  1981. node: m.target,
  1982. attributes: {},
  1983. styleDiff: {},
  1984. _unchangedStyles: {},
  1985. };
  1986. this.attributes.push(item);
  1987. }
  1988. if (attributeName === 'type' &&
  1989. target.tagName === 'INPUT' &&
  1990. (m.oldValue || '').toLowerCase() === 'password') {
  1991. target.setAttribute('data-rr-is-password', 'true');
  1992. }
  1993. if (!ignoreAttribute(target.tagName, attributeName)) {
  1994. item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value, target, this.maskAttributeFn);
  1995. if (attributeName === 'style') {
  1996. const old = unattachedDoc.createElement('span');
  1997. if (m.oldValue) {
  1998. old.setAttribute('style', m.oldValue);
  1999. }
  2000. for (const pname of Array.from(target.style)) {
  2001. const newValue = target.style.getPropertyValue(pname);
  2002. const newPriority = target.style.getPropertyPriority(pname);
  2003. if (newValue !== old.style.getPropertyValue(pname) ||
  2004. newPriority !== old.style.getPropertyPriority(pname)) {
  2005. if (newPriority === '') {
  2006. item.styleDiff[pname] = newValue;
  2007. }
  2008. else {
  2009. item.styleDiff[pname] = [newValue, newPriority];
  2010. }
  2011. }
  2012. else {
  2013. item._unchangedStyles[pname] = [newValue, newPriority];
  2014. }
  2015. }
  2016. for (const pname of Array.from(old.style)) {
  2017. if (target.style.getPropertyValue(pname) === '') {
  2018. item.styleDiff[pname] = false;
  2019. }
  2020. }
  2021. }
  2022. }
  2023. break;
  2024. }
  2025. case 'childList': {
  2026. if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, true)) {
  2027. return;
  2028. }
  2029. m.addedNodes.forEach((n) => this.genAdds(n, m.target));
  2030. m.removedNodes.forEach((n) => {
  2031. const nodeId = this.mirror.getId(n);
  2032. const parentId = isShadowRoot(m.target)
  2033. ? this.mirror.getId(m.target.host)
  2034. : this.mirror.getId(m.target);
  2035. if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) ||
  2036. isIgnored(n, this.mirror) ||
  2037. !isSerialized(n, this.mirror)) {
  2038. return;
  2039. }
  2040. if (this.addedSet.has(n)) {
  2041. deepDelete(this.addedSet, n);
  2042. this.droppedSet.add(n);
  2043. }
  2044. else if (this.addedSet.has(m.target) && nodeId === -1) ;
  2045. else if (isAncestorRemoved(m.target, this.mirror)) ;
  2046. else if (this.movedSet.has(n) &&
  2047. this.movedMap[moveKey(nodeId, parentId)]) {
  2048. deepDelete(this.movedSet, n);
  2049. }
  2050. else {
  2051. this.removes.push({
  2052. parentId,
  2053. id: nodeId,
  2054. isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target)
  2055. ? true
  2056. : undefined,
  2057. });
  2058. }
  2059. this.mapRemoves.push(n);
  2060. });
  2061. break;
  2062. }
  2063. }
  2064. };
  2065. this.genAdds = (n, target) => {
  2066. if (this.processedNodeManager.inOtherBuffer(n, this))
  2067. return;
  2068. if (this.addedSet.has(n) || this.movedSet.has(n))
  2069. return;
  2070. if (this.mirror.hasNode(n)) {
  2071. if (isIgnored(n, this.mirror)) {
  2072. return;
  2073. }
  2074. this.movedSet.add(n);
  2075. let targetId = null;
  2076. if (target && this.mirror.hasNode(target)) {
  2077. targetId = this.mirror.getId(target);
  2078. }
  2079. if (targetId && targetId !== -1) {
  2080. this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true;
  2081. }
  2082. }
  2083. else {
  2084. this.addedSet.add(n);
  2085. this.droppedSet.delete(n);
  2086. }
  2087. if (!isBlocked(n, this.blockClass, this.blockSelector, this.unblockSelector, false)) {
  2088. n.childNodes.forEach((childN) => this.genAdds(childN));
  2089. if (hasShadowRoot(n)) {
  2090. n.shadowRoot.childNodes.forEach((childN) => {
  2091. this.processedNodeManager.add(childN, this);
  2092. this.genAdds(childN, n);
  2093. });
  2094. }
  2095. }
  2096. };
  2097. }
  2098. init(options) {
  2099. [
  2100. 'mutationCb',
  2101. 'blockClass',
  2102. 'blockSelector',
  2103. 'unblockSelector',
  2104. 'maskAllText',
  2105. 'maskTextClass',
  2106. 'unmaskTextClass',
  2107. 'maskTextSelector',
  2108. 'unmaskTextSelector',
  2109. 'inlineStylesheet',
  2110. 'maskInputOptions',
  2111. 'maskAttributeFn',
  2112. 'maskTextFn',
  2113. 'maskInputFn',
  2114. 'keepIframeSrcFn',
  2115. 'recordCanvas',
  2116. 'inlineImages',
  2117. 'slimDOMOptions',
  2118. 'dataURLOptions',
  2119. 'doc',
  2120. 'mirror',
  2121. 'iframeManager',
  2122. 'stylesheetManager',
  2123. 'shadowDomManager',
  2124. 'canvasManager',
  2125. 'processedNodeManager',
  2126. ].forEach((key) => {
  2127. this[key] = options[key];
  2128. });
  2129. }
  2130. freeze() {
  2131. this.frozen = true;
  2132. this.canvasManager.freeze();
  2133. }
  2134. unfreeze() {
  2135. this.frozen = false;
  2136. this.canvasManager.unfreeze();
  2137. this.emit();
  2138. }
  2139. isFrozen() {
  2140. return this.frozen;
  2141. }
  2142. lock() {
  2143. this.locked = true;
  2144. this.canvasManager.lock();
  2145. }
  2146. unlock() {
  2147. this.locked = false;
  2148. this.canvasManager.unlock();
  2149. this.emit();
  2150. }
  2151. reset() {
  2152. this.shadowDomManager.reset();
  2153. this.canvasManager.reset();
  2154. }
  2155. }
  2156. function deepDelete(addsSet, n) {
  2157. addsSet.delete(n);
  2158. n.childNodes.forEach((childN) => deepDelete(addsSet, childN));
  2159. }
  2160. function isParentRemoved(removes, n, mirror) {
  2161. if (removes.length === 0)
  2162. return false;
  2163. return _isParentRemoved(removes, n, mirror);
  2164. }
  2165. function _isParentRemoved(removes, n, mirror) {
  2166. const { parentNode } = n;
  2167. if (!parentNode) {
  2168. return false;
  2169. }
  2170. const parentId = mirror.getId(parentNode);
  2171. if (removes.some((r) => r.id === parentId)) {
  2172. return true;
  2173. }
  2174. return _isParentRemoved(removes, parentNode, mirror);
  2175. }
  2176. function isAncestorInSet(set, n) {
  2177. if (set.size === 0)
  2178. return false;
  2179. return _isAncestorInSet(set, n);
  2180. }
  2181. function _isAncestorInSet(set, n) {
  2182. const { parentNode } = n;
  2183. if (!parentNode) {
  2184. return false;
  2185. }
  2186. if (set.has(parentNode)) {
  2187. return true;
  2188. }
  2189. return _isAncestorInSet(set, parentNode);
  2190. }
  2191. let errorHandler;
  2192. function registerErrorHandler(handler) {
  2193. errorHandler = handler;
  2194. }
  2195. function unregisterErrorHandler() {
  2196. errorHandler = undefined;
  2197. }
  2198. const callbackWrapper = (cb) => {
  2199. if (!errorHandler) {
  2200. return cb;
  2201. }
  2202. const rrwebWrapped = ((...rest) => {
  2203. try {
  2204. return cb(...rest);
  2205. }
  2206. catch (error) {
  2207. if (errorHandler && errorHandler(error) === true) {
  2208. return () => {
  2209. };
  2210. }
  2211. throw error;
  2212. }
  2213. });
  2214. return rrwebWrapped;
  2215. };
  2216. 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; }
  2217. const mutationBuffers = [];
  2218. function getEventTarget(event) {
  2219. try {
  2220. if ('composedPath' in event) {
  2221. const path = event.composedPath();
  2222. if (path.length) {
  2223. return path[0];
  2224. }
  2225. }
  2226. else if ('path' in event && event.path.length) {
  2227. return event.path[0];
  2228. }
  2229. }
  2230. catch (e2) {
  2231. }
  2232. return event && event.target;
  2233. }
  2234. function initMutationObserver(options, rootEl) {
  2235. const mutationBuffer = new MutationBuffer();
  2236. mutationBuffers.push(mutationBuffer);
  2237. mutationBuffer.init(options);
  2238. let mutationObserverCtor = window.MutationObserver ||
  2239. window.__rrMutationObserver;
  2240. const angularZoneSymbol = _optionalChain$2([window, 'optionalAccess', _ => _.Zone, 'optionalAccess', _2 => _2.__symbol__, 'optionalCall', _3 => _3('MutationObserver')]);
  2241. if (angularZoneSymbol &&
  2242. window[angularZoneSymbol]) {
  2243. mutationObserverCtor = window[angularZoneSymbol];
  2244. }
  2245. const observer = new mutationObserverCtor(callbackWrapper((mutations) => {
  2246. if (options.onMutation && options.onMutation(mutations) === false) {
  2247. return;
  2248. }
  2249. mutationBuffer.processMutations.bind(mutationBuffer)(mutations);
  2250. }));
  2251. observer.observe(rootEl, {
  2252. attributes: true,
  2253. attributeOldValue: true,
  2254. characterData: true,
  2255. characterDataOldValue: true,
  2256. childList: true,
  2257. subtree: true,
  2258. });
  2259. return observer;
  2260. }
  2261. function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) {
  2262. if (sampling.mousemove === false) {
  2263. return () => {
  2264. };
  2265. }
  2266. const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50;
  2267. const callbackThreshold = typeof sampling.mousemoveCallback === 'number'
  2268. ? sampling.mousemoveCallback
  2269. : 500;
  2270. let positions = [];
  2271. let timeBaseline;
  2272. const wrappedCb = throttle$1(callbackWrapper((source) => {
  2273. const totalOffset = Date.now() - timeBaseline;
  2274. mousemoveCb(positions.map((p) => {
  2275. p.timeOffset -= totalOffset;
  2276. return p;
  2277. }), source);
  2278. positions = [];
  2279. timeBaseline = null;
  2280. }), callbackThreshold);
  2281. const updatePosition = callbackWrapper(throttle$1(callbackWrapper((evt) => {
  2282. const target = getEventTarget(evt);
  2283. const { clientX, clientY } = legacy_isTouchEvent(evt)
  2284. ? evt.changedTouches[0]
  2285. : evt;
  2286. if (!timeBaseline) {
  2287. timeBaseline = nowTimestamp();
  2288. }
  2289. positions.push({
  2290. x: clientX,
  2291. y: clientY,
  2292. id: mirror.getId(target),
  2293. timeOffset: nowTimestamp() - timeBaseline,
  2294. });
  2295. wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent
  2296. ? IncrementalSource.Drag
  2297. : evt instanceof MouseEvent
  2298. ? IncrementalSource.MouseMove
  2299. : IncrementalSource.TouchMove);
  2300. }), threshold, {
  2301. trailing: false,
  2302. }));
  2303. const handlers = [
  2304. on('mousemove', updatePosition, doc),
  2305. on('touchmove', updatePosition, doc),
  2306. on('drag', updatePosition, doc),
  2307. ];
  2308. return callbackWrapper(() => {
  2309. handlers.forEach((h) => h());
  2310. });
  2311. }
  2312. function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) {
  2313. if (sampling.mouseInteraction === false) {
  2314. return () => {
  2315. };
  2316. }
  2317. const disableMap = sampling.mouseInteraction === true ||
  2318. sampling.mouseInteraction === undefined
  2319. ? {}
  2320. : sampling.mouseInteraction;
  2321. const handlers = [];
  2322. let currentPointerType = null;
  2323. const getHandler = (eventKey) => {
  2324. return (event) => {
  2325. const target = getEventTarget(event);
  2326. if (isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
  2327. return;
  2328. }
  2329. let pointerType = null;
  2330. let thisEventKey = eventKey;
  2331. if ('pointerType' in event) {
  2332. switch (event.pointerType) {
  2333. case 'mouse':
  2334. pointerType = PointerTypes.Mouse;
  2335. break;
  2336. case 'touch':
  2337. pointerType = PointerTypes.Touch;
  2338. break;
  2339. case 'pen':
  2340. pointerType = PointerTypes.Pen;
  2341. break;
  2342. }
  2343. if (pointerType === PointerTypes.Touch) {
  2344. if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) {
  2345. thisEventKey = 'TouchStart';
  2346. }
  2347. else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) {
  2348. thisEventKey = 'TouchEnd';
  2349. }
  2350. }
  2351. else if (pointerType === PointerTypes.Pen) ;
  2352. }
  2353. else if (legacy_isTouchEvent(event)) {
  2354. pointerType = PointerTypes.Touch;
  2355. }
  2356. if (pointerType !== null) {
  2357. currentPointerType = pointerType;
  2358. if ((thisEventKey.startsWith('Touch') &&
  2359. pointerType === PointerTypes.Touch) ||
  2360. (thisEventKey.startsWith('Mouse') &&
  2361. pointerType === PointerTypes.Mouse)) {
  2362. pointerType = null;
  2363. }
  2364. }
  2365. else if (MouseInteractions[eventKey] === MouseInteractions.Click) {
  2366. pointerType = currentPointerType;
  2367. currentPointerType = null;
  2368. }
  2369. const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event;
  2370. if (!e) {
  2371. return;
  2372. }
  2373. const id = mirror.getId(target);
  2374. const { clientX, clientY } = e;
  2375. callbackWrapper(mouseInteractionCb)({
  2376. type: MouseInteractions[thisEventKey],
  2377. id,
  2378. x: clientX,
  2379. y: clientY,
  2380. ...(pointerType !== null && { pointerType }),
  2381. });
  2382. };
  2383. };
  2384. Object.keys(MouseInteractions)
  2385. .filter((key) => Number.isNaN(Number(key)) &&
  2386. !key.endsWith('_Departed') &&
  2387. disableMap[key] !== false)
  2388. .forEach((eventKey) => {
  2389. let eventName = toLowerCase(eventKey);
  2390. const handler = getHandler(eventKey);
  2391. if (window.PointerEvent) {
  2392. switch (MouseInteractions[eventKey]) {
  2393. case MouseInteractions.MouseDown:
  2394. case MouseInteractions.MouseUp:
  2395. eventName = eventName.replace('mouse', 'pointer');
  2396. break;
  2397. case MouseInteractions.TouchStart:
  2398. case MouseInteractions.TouchEnd:
  2399. return;
  2400. }
  2401. }
  2402. handlers.push(on(eventName, handler, doc));
  2403. });
  2404. return callbackWrapper(() => {
  2405. handlers.forEach((h) => h());
  2406. });
  2407. }
  2408. function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) {
  2409. const updatePosition = callbackWrapper(throttle$1(callbackWrapper((evt) => {
  2410. const target = getEventTarget(evt);
  2411. if (!target ||
  2412. isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
  2413. return;
  2414. }
  2415. const id = mirror.getId(target);
  2416. if (target === doc && doc.defaultView) {
  2417. const scrollLeftTop = getWindowScroll(doc.defaultView);
  2418. scrollCb({
  2419. id,
  2420. x: scrollLeftTop.left,
  2421. y: scrollLeftTop.top,
  2422. });
  2423. }
  2424. else {
  2425. scrollCb({
  2426. id,
  2427. x: target.scrollLeft,
  2428. y: target.scrollTop,
  2429. });
  2430. }
  2431. }), sampling.scroll || 100));
  2432. return on('scroll', updatePosition, doc);
  2433. }
  2434. function initViewportResizeObserver({ viewportResizeCb }, { win }) {
  2435. let lastH = -1;
  2436. let lastW = -1;
  2437. const updateDimension = callbackWrapper(throttle$1(callbackWrapper(() => {
  2438. const height = getWindowHeight();
  2439. const width = getWindowWidth();
  2440. if (lastH !== height || lastW !== width) {
  2441. viewportResizeCb({
  2442. width: Number(width),
  2443. height: Number(height),
  2444. });
  2445. lastH = height;
  2446. lastW = width;
  2447. }
  2448. }), 200));
  2449. return on('resize', updateDimension, win);
  2450. }
  2451. const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
  2452. const lastInputValueMap = new WeakMap();
  2453. function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, unblockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, }) {
  2454. function eventHandler(event) {
  2455. let target = getEventTarget(event);
  2456. const userTriggered = event.isTrusted;
  2457. const tagName = target && toUpperCase(target.tagName);
  2458. if (tagName === 'OPTION')
  2459. target = target.parentElement;
  2460. if (!target ||
  2461. !tagName ||
  2462. INPUT_TAGS.indexOf(tagName) < 0 ||
  2463. isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
  2464. return;
  2465. }
  2466. const el = target;
  2467. if (el.classList.contains(ignoreClass) ||
  2468. (ignoreSelector && el.matches(ignoreSelector))) {
  2469. return;
  2470. }
  2471. const type = getInputType(target);
  2472. let text = getInputValue(el, tagName, type);
  2473. let isChecked = false;
  2474. const isInputMasked = shouldMaskInput({
  2475. maskInputOptions,
  2476. tagName,
  2477. type,
  2478. });
  2479. const forceMask = needMaskingText(target, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, isInputMasked);
  2480. if (type === 'radio' || type === 'checkbox') {
  2481. isChecked = target.checked;
  2482. }
  2483. text = maskInputValue({
  2484. isMasked: forceMask,
  2485. element: target,
  2486. value: text,
  2487. maskInputFn,
  2488. });
  2489. cbWithDedup(target, userTriggeredOnInput
  2490. ? { text, isChecked, userTriggered }
  2491. : { text, isChecked });
  2492. const name = target.name;
  2493. if (type === 'radio' && name && isChecked) {
  2494. doc
  2495. .querySelectorAll(`input[type="radio"][name="${name}"]`)
  2496. .forEach((el) => {
  2497. if (el !== target) {
  2498. const text = maskInputValue({
  2499. isMasked: forceMask,
  2500. element: el,
  2501. value: getInputValue(el, tagName, type),
  2502. maskInputFn,
  2503. });
  2504. cbWithDedup(el, userTriggeredOnInput
  2505. ? { text, isChecked: !isChecked, userTriggered: false }
  2506. : { text, isChecked: !isChecked });
  2507. }
  2508. });
  2509. }
  2510. }
  2511. function cbWithDedup(target, v) {
  2512. const lastInputValue = lastInputValueMap.get(target);
  2513. if (!lastInputValue ||
  2514. lastInputValue.text !== v.text ||
  2515. lastInputValue.isChecked !== v.isChecked) {
  2516. lastInputValueMap.set(target, v);
  2517. const id = mirror.getId(target);
  2518. callbackWrapper(inputCb)({
  2519. ...v,
  2520. id,
  2521. });
  2522. }
  2523. }
  2524. const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
  2525. const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc));
  2526. const currentWindow = doc.defaultView;
  2527. if (!currentWindow) {
  2528. return () => {
  2529. handlers.forEach((h) => h());
  2530. };
  2531. }
  2532. const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value');
  2533. const hookProperties = [
  2534. [currentWindow.HTMLInputElement.prototype, 'value'],
  2535. [currentWindow.HTMLInputElement.prototype, 'checked'],
  2536. [currentWindow.HTMLSelectElement.prototype, 'value'],
  2537. [currentWindow.HTMLTextAreaElement.prototype, 'value'],
  2538. [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'],
  2539. [currentWindow.HTMLOptionElement.prototype, 'selected'],
  2540. ];
  2541. if (propertyDescriptor && propertyDescriptor.set) {
  2542. handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], {
  2543. set() {
  2544. callbackWrapper(eventHandler)({
  2545. target: this,
  2546. isTrusted: false,
  2547. });
  2548. },
  2549. }, false, currentWindow)));
  2550. }
  2551. return callbackWrapper(() => {
  2552. handlers.forEach((h) => h());
  2553. });
  2554. }
  2555. function getNestedCSSRulePositions(rule) {
  2556. const positions = [];
  2557. function recurse(childRule, pos) {
  2558. if ((hasNestedCSSRule('CSSGroupingRule') &&
  2559. childRule.parentRule instanceof CSSGroupingRule) ||
  2560. (hasNestedCSSRule('CSSMediaRule') &&
  2561. childRule.parentRule instanceof CSSMediaRule) ||
  2562. (hasNestedCSSRule('CSSSupportsRule') &&
  2563. childRule.parentRule instanceof CSSSupportsRule) ||
  2564. (hasNestedCSSRule('CSSConditionRule') &&
  2565. childRule.parentRule instanceof CSSConditionRule)) {
  2566. const rules = Array.from(childRule.parentRule.cssRules);
  2567. const index = rules.indexOf(childRule);
  2568. pos.unshift(index);
  2569. }
  2570. else if (childRule.parentStyleSheet) {
  2571. const rules = Array.from(childRule.parentStyleSheet.cssRules);
  2572. const index = rules.indexOf(childRule);
  2573. pos.unshift(index);
  2574. }
  2575. return pos;
  2576. }
  2577. return recurse(rule, positions);
  2578. }
  2579. function getIdAndStyleId(sheet, mirror, styleMirror) {
  2580. let id, styleId;
  2581. if (!sheet)
  2582. return {};
  2583. if (sheet.ownerNode)
  2584. id = mirror.getId(sheet.ownerNode);
  2585. else
  2586. styleId = styleMirror.getId(sheet);
  2587. return {
  2588. styleId,
  2589. id,
  2590. };
  2591. }
  2592. function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) {
  2593. if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) {
  2594. return () => {
  2595. };
  2596. }
  2597. const insertRule = win.CSSStyleSheet.prototype.insertRule;
  2598. win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, {
  2599. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2600. const [rule, index] = argumentsList;
  2601. const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
  2602. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2603. styleSheetRuleCb({
  2604. id,
  2605. styleId,
  2606. adds: [{ rule, index }],
  2607. });
  2608. }
  2609. return target.apply(thisArg, argumentsList);
  2610. }),
  2611. });
  2612. const deleteRule = win.CSSStyleSheet.prototype.deleteRule;
  2613. win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, {
  2614. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2615. const [index] = argumentsList;
  2616. const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
  2617. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2618. styleSheetRuleCb({
  2619. id,
  2620. styleId,
  2621. removes: [{ index }],
  2622. });
  2623. }
  2624. return target.apply(thisArg, argumentsList);
  2625. }),
  2626. });
  2627. let replace;
  2628. if (win.CSSStyleSheet.prototype.replace) {
  2629. replace = win.CSSStyleSheet.prototype.replace;
  2630. win.CSSStyleSheet.prototype.replace = new Proxy(replace, {
  2631. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2632. const [text] = argumentsList;
  2633. const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
  2634. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2635. styleSheetRuleCb({
  2636. id,
  2637. styleId,
  2638. replace: text,
  2639. });
  2640. }
  2641. return target.apply(thisArg, argumentsList);
  2642. }),
  2643. });
  2644. }
  2645. let replaceSync;
  2646. if (win.CSSStyleSheet.prototype.replaceSync) {
  2647. replaceSync = win.CSSStyleSheet.prototype.replaceSync;
  2648. win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, {
  2649. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2650. const [text] = argumentsList;
  2651. const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
  2652. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2653. styleSheetRuleCb({
  2654. id,
  2655. styleId,
  2656. replaceSync: text,
  2657. });
  2658. }
  2659. return target.apply(thisArg, argumentsList);
  2660. }),
  2661. });
  2662. }
  2663. const supportedNestedCSSRuleTypes = {};
  2664. if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) {
  2665. supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule;
  2666. }
  2667. else {
  2668. if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) {
  2669. supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule;
  2670. }
  2671. if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) {
  2672. supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule;
  2673. }
  2674. if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) {
  2675. supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule;
  2676. }
  2677. }
  2678. const unmodifiedFunctions = {};
  2679. Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
  2680. unmodifiedFunctions[typeKey] = {
  2681. insertRule: type.prototype.insertRule,
  2682. deleteRule: type.prototype.deleteRule,
  2683. };
  2684. type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, {
  2685. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2686. const [rule, index] = argumentsList;
  2687. const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror);
  2688. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2689. styleSheetRuleCb({
  2690. id,
  2691. styleId,
  2692. adds: [
  2693. {
  2694. rule,
  2695. index: [
  2696. ...getNestedCSSRulePositions(thisArg),
  2697. index || 0,
  2698. ],
  2699. },
  2700. ],
  2701. });
  2702. }
  2703. return target.apply(thisArg, argumentsList);
  2704. }),
  2705. });
  2706. type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, {
  2707. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2708. const [index] = argumentsList;
  2709. const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror);
  2710. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2711. styleSheetRuleCb({
  2712. id,
  2713. styleId,
  2714. removes: [
  2715. { index: [...getNestedCSSRulePositions(thisArg), index] },
  2716. ],
  2717. });
  2718. }
  2719. return target.apply(thisArg, argumentsList);
  2720. }),
  2721. });
  2722. });
  2723. return callbackWrapper(() => {
  2724. win.CSSStyleSheet.prototype.insertRule = insertRule;
  2725. win.CSSStyleSheet.prototype.deleteRule = deleteRule;
  2726. replace && (win.CSSStyleSheet.prototype.replace = replace);
  2727. replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync);
  2728. Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
  2729. type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
  2730. type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
  2731. });
  2732. });
  2733. }
  2734. function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) {
  2735. let hostId = null;
  2736. if (host.nodeName === '#document')
  2737. hostId = mirror.getId(host);
  2738. else
  2739. hostId = mirror.getId(host.host);
  2740. const patchTarget = host.nodeName === '#document'
  2741. ? _optionalChain$2([host, 'access', _4 => _4.defaultView, 'optionalAccess', _5 => _5.Document])
  2742. : _optionalChain$2([host, 'access', _6 => _6.ownerDocument, 'optionalAccess', _7 => _7.defaultView, 'optionalAccess', _8 => _8.ShadowRoot]);
  2743. const originalPropertyDescriptor = _optionalChain$2([patchTarget, 'optionalAccess', _9 => _9.prototype])
  2744. ? Object.getOwnPropertyDescriptor(_optionalChain$2([patchTarget, 'optionalAccess', _10 => _10.prototype]), 'adoptedStyleSheets')
  2745. : undefined;
  2746. if (hostId === null ||
  2747. hostId === -1 ||
  2748. !patchTarget ||
  2749. !originalPropertyDescriptor)
  2750. return () => {
  2751. };
  2752. Object.defineProperty(host, 'adoptedStyleSheets', {
  2753. configurable: originalPropertyDescriptor.configurable,
  2754. enumerable: originalPropertyDescriptor.enumerable,
  2755. get() {
  2756. return _optionalChain$2([originalPropertyDescriptor, 'access', _11 => _11.get, 'optionalAccess', _12 => _12.call, 'call', _13 => _13(this)]);
  2757. },
  2758. set(sheets) {
  2759. const result = _optionalChain$2([originalPropertyDescriptor, 'access', _14 => _14.set, 'optionalAccess', _15 => _15.call, 'call', _16 => _16(this, sheets)]);
  2760. if (hostId !== null && hostId !== -1) {
  2761. try {
  2762. stylesheetManager.adoptStyleSheets(sheets, hostId);
  2763. }
  2764. catch (e) {
  2765. }
  2766. }
  2767. return result;
  2768. },
  2769. });
  2770. return callbackWrapper(() => {
  2771. Object.defineProperty(host, 'adoptedStyleSheets', {
  2772. configurable: originalPropertyDescriptor.configurable,
  2773. enumerable: originalPropertyDescriptor.enumerable,
  2774. get: originalPropertyDescriptor.get,
  2775. set: originalPropertyDescriptor.set,
  2776. });
  2777. });
  2778. }
  2779. function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) {
  2780. const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
  2781. win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, {
  2782. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2783. const [property, value, priority] = argumentsList;
  2784. if (ignoreCSSAttributes.has(property)) {
  2785. return setProperty.apply(thisArg, [property, value, priority]);
  2786. }
  2787. const { id, styleId } = getIdAndStyleId(_optionalChain$2([thisArg, 'access', _17 => _17.parentRule, 'optionalAccess', _18 => _18.parentStyleSheet]), mirror, stylesheetManager.styleMirror);
  2788. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2789. styleDeclarationCb({
  2790. id,
  2791. styleId,
  2792. set: {
  2793. property,
  2794. value,
  2795. priority,
  2796. },
  2797. index: getNestedCSSRulePositions(thisArg.parentRule),
  2798. });
  2799. }
  2800. return target.apply(thisArg, argumentsList);
  2801. }),
  2802. });
  2803. const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty;
  2804. win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, {
  2805. apply: callbackWrapper((target, thisArg, argumentsList) => {
  2806. const [property] = argumentsList;
  2807. if (ignoreCSSAttributes.has(property)) {
  2808. return removeProperty.apply(thisArg, [property]);
  2809. }
  2810. const { id, styleId } = getIdAndStyleId(_optionalChain$2([thisArg, 'access', _19 => _19.parentRule, 'optionalAccess', _20 => _20.parentStyleSheet]), mirror, stylesheetManager.styleMirror);
  2811. if ((id && id !== -1) || (styleId && styleId !== -1)) {
  2812. styleDeclarationCb({
  2813. id,
  2814. styleId,
  2815. remove: {
  2816. property,
  2817. },
  2818. index: getNestedCSSRulePositions(thisArg.parentRule),
  2819. });
  2820. }
  2821. return target.apply(thisArg, argumentsList);
  2822. }),
  2823. });
  2824. return callbackWrapper(() => {
  2825. win.CSSStyleDeclaration.prototype.setProperty = setProperty;
  2826. win.CSSStyleDeclaration.prototype.removeProperty = removeProperty;
  2827. });
  2828. }
  2829. function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, unblockSelector, mirror, sampling, doc, }) {
  2830. const handler = callbackWrapper((type) => throttle$1(callbackWrapper((event) => {
  2831. const target = getEventTarget(event);
  2832. if (!target ||
  2833. isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
  2834. return;
  2835. }
  2836. const { currentTime, volume, muted, playbackRate } = target;
  2837. mediaInteractionCb({
  2838. type,
  2839. id: mirror.getId(target),
  2840. currentTime,
  2841. volume,
  2842. muted,
  2843. playbackRate,
  2844. });
  2845. }), sampling.media || 500));
  2846. const handlers = [
  2847. on('play', handler(0), doc),
  2848. on('pause', handler(1), doc),
  2849. on('seeked', handler(2), doc),
  2850. on('volumechange', handler(3), doc),
  2851. on('ratechange', handler(4), doc),
  2852. ];
  2853. return callbackWrapper(() => {
  2854. handlers.forEach((h) => h());
  2855. });
  2856. }
  2857. function initFontObserver({ fontCb, doc }) {
  2858. const win = doc.defaultView;
  2859. if (!win) {
  2860. return () => {
  2861. };
  2862. }
  2863. const handlers = [];
  2864. const fontMap = new WeakMap();
  2865. const originalFontFace = win.FontFace;
  2866. win.FontFace = function FontFace(family, source, descriptors) {
  2867. const fontFace = new originalFontFace(family, source, descriptors);
  2868. fontMap.set(fontFace, {
  2869. family,
  2870. buffer: typeof source !== 'string',
  2871. descriptors,
  2872. fontSource: typeof source === 'string'
  2873. ? source
  2874. : JSON.stringify(Array.from(new Uint8Array(source))),
  2875. });
  2876. return fontFace;
  2877. };
  2878. const restoreHandler = patch(doc.fonts, 'add', function (original) {
  2879. return function (fontFace) {
  2880. setTimeout(callbackWrapper(() => {
  2881. const p = fontMap.get(fontFace);
  2882. if (p) {
  2883. fontCb(p);
  2884. fontMap.delete(fontFace);
  2885. }
  2886. }), 0);
  2887. return original.apply(this, [fontFace]);
  2888. };
  2889. });
  2890. handlers.push(() => {
  2891. win.FontFace = originalFontFace;
  2892. });
  2893. handlers.push(restoreHandler);
  2894. return callbackWrapper(() => {
  2895. handlers.forEach((h) => h());
  2896. });
  2897. }
  2898. function initSelectionObserver(param) {
  2899. const { doc, mirror, blockClass, blockSelector, unblockSelector, selectionCb, } = param;
  2900. let collapsed = true;
  2901. const updateSelection = callbackWrapper(() => {
  2902. const selection = doc.getSelection();
  2903. if (!selection || (collapsed && _optionalChain$2([selection, 'optionalAccess', _21 => _21.isCollapsed])))
  2904. return;
  2905. collapsed = selection.isCollapsed || false;
  2906. const ranges = [];
  2907. const count = selection.rangeCount || 0;
  2908. for (let i = 0; i < count; i++) {
  2909. const range = selection.getRangeAt(i);
  2910. const { startContainer, startOffset, endContainer, endOffset } = range;
  2911. const blocked = isBlocked(startContainer, blockClass, blockSelector, unblockSelector, true) ||
  2912. isBlocked(endContainer, blockClass, blockSelector, unblockSelector, true);
  2913. if (blocked)
  2914. continue;
  2915. ranges.push({
  2916. start: mirror.getId(startContainer),
  2917. startOffset,
  2918. end: mirror.getId(endContainer),
  2919. endOffset,
  2920. });
  2921. }
  2922. selectionCb({ ranges });
  2923. });
  2924. updateSelection();
  2925. return on('selectionchange', updateSelection);
  2926. }
  2927. function initCustomElementObserver({ doc, customElementCb, }) {
  2928. const win = doc.defaultView;
  2929. if (!win || !win.customElements)
  2930. return () => { };
  2931. const restoreHandler = patch(win.customElements, 'define', function (original) {
  2932. return function (name, constructor, options) {
  2933. try {
  2934. customElementCb({
  2935. define: {
  2936. name,
  2937. },
  2938. });
  2939. }
  2940. catch (e) {
  2941. }
  2942. return original.apply(this, [name, constructor, options]);
  2943. };
  2944. });
  2945. return restoreHandler;
  2946. }
  2947. function initObservers(o, _hooks = {}) {
  2948. const currentWindow = o.doc.defaultView;
  2949. if (!currentWindow) {
  2950. return () => {
  2951. };
  2952. }
  2953. const mutationObserver = initMutationObserver(o, o.doc);
  2954. const mousemoveHandler = initMoveObserver(o);
  2955. const mouseInteractionHandler = initMouseInteractionObserver(o);
  2956. const scrollHandler = initScrollObserver(o);
  2957. const viewportResizeHandler = initViewportResizeObserver(o, {
  2958. win: currentWindow,
  2959. });
  2960. const inputHandler = initInputObserver(o);
  2961. const mediaInteractionHandler = initMediaInteractionObserver(o);
  2962. const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow });
  2963. const adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc);
  2964. const styleDeclarationObserver = initStyleDeclarationObserver(o, {
  2965. win: currentWindow,
  2966. });
  2967. const fontObserver = o.collectFonts
  2968. ? initFontObserver(o)
  2969. : () => {
  2970. };
  2971. const selectionObserver = initSelectionObserver(o);
  2972. const customElementObserver = initCustomElementObserver(o);
  2973. const pluginHandlers = [];
  2974. for (const plugin of o.plugins) {
  2975. pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options));
  2976. }
  2977. return callbackWrapper(() => {
  2978. mutationBuffers.forEach((b) => b.reset());
  2979. mutationObserver.disconnect();
  2980. mousemoveHandler();
  2981. mouseInteractionHandler();
  2982. scrollHandler();
  2983. viewportResizeHandler();
  2984. inputHandler();
  2985. mediaInteractionHandler();
  2986. styleSheetObserver();
  2987. adoptedStyleSheetObserver();
  2988. styleDeclarationObserver();
  2989. fontObserver();
  2990. selectionObserver();
  2991. customElementObserver();
  2992. pluginHandlers.forEach((h) => h());
  2993. });
  2994. }
  2995. function hasNestedCSSRule(prop) {
  2996. return typeof window[prop] !== 'undefined';
  2997. }
  2998. function canMonkeyPatchNestedCSSRule(prop) {
  2999. return Boolean(typeof window[prop] !== 'undefined' &&
  3000. window[prop].prototype &&
  3001. 'insertRule' in window[prop].prototype &&
  3002. 'deleteRule' in window[prop].prototype);
  3003. }
  3004. class CrossOriginIframeMirror {
  3005. constructor(generateIdFn) {
  3006. this.generateIdFn = generateIdFn;
  3007. this.iframeIdToRemoteIdMap = new WeakMap();
  3008. this.iframeRemoteIdToIdMap = new WeakMap();
  3009. }
  3010. getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) {
  3011. const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe);
  3012. const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe);
  3013. let id = idToRemoteIdMap.get(remoteId);
  3014. if (!id) {
  3015. id = this.generateIdFn();
  3016. idToRemoteIdMap.set(remoteId, id);
  3017. remoteIdToIdMap.set(id, remoteId);
  3018. }
  3019. return id;
  3020. }
  3021. getIds(iframe, remoteId) {
  3022. const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe);
  3023. const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
  3024. return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap));
  3025. }
  3026. getRemoteId(iframe, id, map) {
  3027. const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe);
  3028. if (typeof id !== 'number')
  3029. return id;
  3030. const remoteId = remoteIdToIdMap.get(id);
  3031. if (!remoteId)
  3032. return -1;
  3033. return remoteId;
  3034. }
  3035. getRemoteIds(iframe, ids) {
  3036. const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
  3037. return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap));
  3038. }
  3039. reset(iframe) {
  3040. if (!iframe) {
  3041. this.iframeIdToRemoteIdMap = new WeakMap();
  3042. this.iframeRemoteIdToIdMap = new WeakMap();
  3043. return;
  3044. }
  3045. this.iframeIdToRemoteIdMap.delete(iframe);
  3046. this.iframeRemoteIdToIdMap.delete(iframe);
  3047. }
  3048. getIdToRemoteIdMap(iframe) {
  3049. let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe);
  3050. if (!idToRemoteIdMap) {
  3051. idToRemoteIdMap = new Map();
  3052. this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap);
  3053. }
  3054. return idToRemoteIdMap;
  3055. }
  3056. getRemoteIdToIdMap(iframe) {
  3057. let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe);
  3058. if (!remoteIdToIdMap) {
  3059. remoteIdToIdMap = new Map();
  3060. this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap);
  3061. }
  3062. return remoteIdToIdMap;
  3063. }
  3064. }
  3065. 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; }
  3066. class IframeManagerNoop {
  3067. constructor() {
  3068. this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
  3069. this.crossOriginIframeRootIdMap = new WeakMap();
  3070. }
  3071. addIframe() {
  3072. }
  3073. addLoadListener() {
  3074. }
  3075. attachIframe() {
  3076. }
  3077. }
  3078. class IframeManager {
  3079. constructor(options) {
  3080. this.iframes = new WeakMap();
  3081. this.crossOriginIframeMap = new WeakMap();
  3082. this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
  3083. this.crossOriginIframeRootIdMap = new WeakMap();
  3084. this.mutationCb = options.mutationCb;
  3085. this.wrappedEmit = options.wrappedEmit;
  3086. this.stylesheetManager = options.stylesheetManager;
  3087. this.recordCrossOriginIframes = options.recordCrossOriginIframes;
  3088. this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror));
  3089. this.mirror = options.mirror;
  3090. if (this.recordCrossOriginIframes) {
  3091. window.addEventListener('message', this.handleMessage.bind(this));
  3092. }
  3093. }
  3094. addIframe(iframeEl) {
  3095. this.iframes.set(iframeEl, true);
  3096. if (iframeEl.contentWindow)
  3097. this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
  3098. }
  3099. addLoadListener(cb) {
  3100. this.loadListener = cb;
  3101. }
  3102. attachIframe(iframeEl, childSn) {
  3103. this.mutationCb({
  3104. adds: [
  3105. {
  3106. parentId: this.mirror.getId(iframeEl),
  3107. nextId: null,
  3108. node: childSn,
  3109. },
  3110. ],
  3111. removes: [],
  3112. texts: [],
  3113. attributes: [],
  3114. isAttachIframe: true,
  3115. });
  3116. _optionalChain$1([this, 'access', _ => _.loadListener, 'optionalCall', _2 => _2(iframeEl)]);
  3117. if (iframeEl.contentDocument &&
  3118. iframeEl.contentDocument.adoptedStyleSheets &&
  3119. iframeEl.contentDocument.adoptedStyleSheets.length > 0)
  3120. this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
  3121. }
  3122. handleMessage(message) {
  3123. const crossOriginMessageEvent = message;
  3124. if (crossOriginMessageEvent.data.type !== 'rrweb' ||
  3125. crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin)
  3126. return;
  3127. const iframeSourceWindow = message.source;
  3128. if (!iframeSourceWindow)
  3129. return;
  3130. const iframeEl = this.crossOriginIframeMap.get(message.source);
  3131. if (!iframeEl)
  3132. return;
  3133. const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event);
  3134. if (transformedEvent)
  3135. this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout);
  3136. }
  3137. transformCrossOriginEvent(iframeEl, e) {
  3138. switch (e.type) {
  3139. case EventType.FullSnapshot: {
  3140. this.crossOriginIframeMirror.reset(iframeEl);
  3141. this.crossOriginIframeStyleMirror.reset(iframeEl);
  3142. this.replaceIdOnNode(e.data.node, iframeEl);
  3143. const rootId = e.data.node.id;
  3144. this.crossOriginIframeRootIdMap.set(iframeEl, rootId);
  3145. this.patchRootIdOnNode(e.data.node, rootId);
  3146. return {
  3147. timestamp: e.timestamp,
  3148. type: EventType.IncrementalSnapshot,
  3149. data: {
  3150. source: IncrementalSource.Mutation,
  3151. adds: [
  3152. {
  3153. parentId: this.mirror.getId(iframeEl),
  3154. nextId: null,
  3155. node: e.data.node,
  3156. },
  3157. ],
  3158. removes: [],
  3159. texts: [],
  3160. attributes: [],
  3161. isAttachIframe: true,
  3162. },
  3163. };
  3164. }
  3165. case EventType.Meta:
  3166. case EventType.Load:
  3167. case EventType.DomContentLoaded: {
  3168. return false;
  3169. }
  3170. case EventType.Plugin: {
  3171. return e;
  3172. }
  3173. case EventType.Custom: {
  3174. this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']);
  3175. return e;
  3176. }
  3177. case EventType.IncrementalSnapshot: {
  3178. switch (e.data.source) {
  3179. case IncrementalSource.Mutation: {
  3180. e.data.adds.forEach((n) => {
  3181. this.replaceIds(n, iframeEl, [
  3182. 'parentId',
  3183. 'nextId',
  3184. 'previousId',
  3185. ]);
  3186. this.replaceIdOnNode(n.node, iframeEl);
  3187. const rootId = this.crossOriginIframeRootIdMap.get(iframeEl);
  3188. rootId && this.patchRootIdOnNode(n.node, rootId);
  3189. });
  3190. e.data.removes.forEach((n) => {
  3191. this.replaceIds(n, iframeEl, ['parentId', 'id']);
  3192. });
  3193. e.data.attributes.forEach((n) => {
  3194. this.replaceIds(n, iframeEl, ['id']);
  3195. });
  3196. e.data.texts.forEach((n) => {
  3197. this.replaceIds(n, iframeEl, ['id']);
  3198. });
  3199. return e;
  3200. }
  3201. case IncrementalSource.Drag:
  3202. case IncrementalSource.TouchMove:
  3203. case IncrementalSource.MouseMove: {
  3204. e.data.positions.forEach((p) => {
  3205. this.replaceIds(p, iframeEl, ['id']);
  3206. });
  3207. return e;
  3208. }
  3209. case IncrementalSource.ViewportResize: {
  3210. return false;
  3211. }
  3212. case IncrementalSource.MediaInteraction:
  3213. case IncrementalSource.MouseInteraction:
  3214. case IncrementalSource.Scroll:
  3215. case IncrementalSource.CanvasMutation:
  3216. case IncrementalSource.Input: {
  3217. this.replaceIds(e.data, iframeEl, ['id']);
  3218. return e;
  3219. }
  3220. case IncrementalSource.StyleSheetRule:
  3221. case IncrementalSource.StyleDeclaration: {
  3222. this.replaceIds(e.data, iframeEl, ['id']);
  3223. this.replaceStyleIds(e.data, iframeEl, ['styleId']);
  3224. return e;
  3225. }
  3226. case IncrementalSource.Font: {
  3227. return e;
  3228. }
  3229. case IncrementalSource.Selection: {
  3230. e.data.ranges.forEach((range) => {
  3231. this.replaceIds(range, iframeEl, ['start', 'end']);
  3232. });
  3233. return e;
  3234. }
  3235. case IncrementalSource.AdoptedStyleSheet: {
  3236. this.replaceIds(e.data, iframeEl, ['id']);
  3237. this.replaceStyleIds(e.data, iframeEl, ['styleIds']);
  3238. _optionalChain$1([e, 'access', _3 => _3.data, 'access', _4 => _4.styles, 'optionalAccess', _5 => _5.forEach, 'call', _6 => _6((style) => {
  3239. this.replaceStyleIds(style, iframeEl, ['styleId']);
  3240. })]);
  3241. return e;
  3242. }
  3243. }
  3244. }
  3245. }
  3246. return false;
  3247. }
  3248. replace(iframeMirror, obj, iframeEl, keys) {
  3249. for (const key of keys) {
  3250. if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number')
  3251. continue;
  3252. if (Array.isArray(obj[key])) {
  3253. obj[key] = iframeMirror.getIds(iframeEl, obj[key]);
  3254. }
  3255. else {
  3256. obj[key] = iframeMirror.getId(iframeEl, obj[key]);
  3257. }
  3258. }
  3259. return obj;
  3260. }
  3261. replaceIds(obj, iframeEl, keys) {
  3262. return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys);
  3263. }
  3264. replaceStyleIds(obj, iframeEl, keys) {
  3265. return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys);
  3266. }
  3267. replaceIdOnNode(node, iframeEl) {
  3268. this.replaceIds(node, iframeEl, ['id', 'rootId']);
  3269. if ('childNodes' in node) {
  3270. node.childNodes.forEach((child) => {
  3271. this.replaceIdOnNode(child, iframeEl);
  3272. });
  3273. }
  3274. }
  3275. patchRootIdOnNode(node, rootId) {
  3276. if (node.type !== NodeType$1.Document && !node.rootId)
  3277. node.rootId = rootId;
  3278. if ('childNodes' in node) {
  3279. node.childNodes.forEach((child) => {
  3280. this.patchRootIdOnNode(child, rootId);
  3281. });
  3282. }
  3283. }
  3284. }
  3285. class ShadowDomManagerNoop {
  3286. init() {
  3287. }
  3288. addShadowRoot() {
  3289. }
  3290. observeAttachShadow() {
  3291. }
  3292. reset() {
  3293. }
  3294. }
  3295. class ShadowDomManager {
  3296. constructor(options) {
  3297. this.shadowDoms = new WeakSet();
  3298. this.restoreHandlers = [];
  3299. this.mutationCb = options.mutationCb;
  3300. this.scrollCb = options.scrollCb;
  3301. this.bypassOptions = options.bypassOptions;
  3302. this.mirror = options.mirror;
  3303. this.init();
  3304. }
  3305. init() {
  3306. this.reset();
  3307. this.patchAttachShadow(Element, document);
  3308. }
  3309. addShadowRoot(shadowRoot, doc) {
  3310. if (!isNativeShadowDom(shadowRoot))
  3311. return;
  3312. if (this.shadowDoms.has(shadowRoot))
  3313. return;
  3314. this.shadowDoms.add(shadowRoot);
  3315. const observer = initMutationObserver({
  3316. ...this.bypassOptions,
  3317. doc,
  3318. mutationCb: this.mutationCb,
  3319. mirror: this.mirror,
  3320. shadowDomManager: this,
  3321. }, shadowRoot);
  3322. this.restoreHandlers.push(() => observer.disconnect());
  3323. this.restoreHandlers.push(initScrollObserver({
  3324. ...this.bypassOptions,
  3325. scrollCb: this.scrollCb,
  3326. doc: shadowRoot,
  3327. mirror: this.mirror,
  3328. }));
  3329. setTimeout(() => {
  3330. if (shadowRoot.adoptedStyleSheets &&
  3331. shadowRoot.adoptedStyleSheets.length > 0)
  3332. this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host));
  3333. this.restoreHandlers.push(initAdoptedStyleSheetObserver({
  3334. mirror: this.mirror,
  3335. stylesheetManager: this.bypassOptions.stylesheetManager,
  3336. }, shadowRoot));
  3337. }, 0);
  3338. }
  3339. observeAttachShadow(iframeElement) {
  3340. if (!iframeElement.contentWindow || !iframeElement.contentDocument)
  3341. return;
  3342. this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument);
  3343. }
  3344. patchAttachShadow(element, doc) {
  3345. const manager = this;
  3346. this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) {
  3347. return function (option) {
  3348. const shadowRoot = original.call(this, option);
  3349. if (this.shadowRoot && inDom(this))
  3350. manager.addShadowRoot(this.shadowRoot, doc);
  3351. return shadowRoot;
  3352. };
  3353. }));
  3354. }
  3355. reset() {
  3356. this.restoreHandlers.forEach((handler) => {
  3357. try {
  3358. handler();
  3359. }
  3360. catch (e) {
  3361. }
  3362. });
  3363. this.restoreHandlers = [];
  3364. this.shadowDoms = new WeakSet();
  3365. }
  3366. }
  3367. class CanvasManagerNoop {
  3368. reset() {
  3369. }
  3370. freeze() {
  3371. }
  3372. unfreeze() {
  3373. }
  3374. lock() {
  3375. }
  3376. unlock() {
  3377. }
  3378. snapshot() {
  3379. }
  3380. }
  3381. class StylesheetManager {
  3382. constructor(options) {
  3383. this.trackedLinkElements = new WeakSet();
  3384. this.styleMirror = new StyleSheetMirror();
  3385. this.mutationCb = options.mutationCb;
  3386. this.adoptedStyleSheetCb = options.adoptedStyleSheetCb;
  3387. }
  3388. attachLinkElement(linkEl, childSn) {
  3389. if ('_cssText' in childSn.attributes)
  3390. this.mutationCb({
  3391. adds: [],
  3392. removes: [],
  3393. texts: [],
  3394. attributes: [
  3395. {
  3396. id: childSn.id,
  3397. attributes: childSn
  3398. .attributes,
  3399. },
  3400. ],
  3401. });
  3402. this.trackLinkElement(linkEl);
  3403. }
  3404. trackLinkElement(linkEl) {
  3405. if (this.trackedLinkElements.has(linkEl))
  3406. return;
  3407. this.trackedLinkElements.add(linkEl);
  3408. this.trackStylesheetInLinkElement(linkEl);
  3409. }
  3410. adoptStyleSheets(sheets, hostId) {
  3411. if (sheets.length === 0)
  3412. return;
  3413. const adoptedStyleSheetData = {
  3414. id: hostId,
  3415. styleIds: [],
  3416. };
  3417. const styles = [];
  3418. for (const sheet of sheets) {
  3419. let styleId;
  3420. if (!this.styleMirror.has(sheet)) {
  3421. styleId = this.styleMirror.add(sheet);
  3422. styles.push({
  3423. styleId,
  3424. rules: Array.from(sheet.rules || CSSRule, (r, index) => ({
  3425. rule: stringifyRule(r),
  3426. index,
  3427. })),
  3428. });
  3429. }
  3430. else
  3431. styleId = this.styleMirror.getId(sheet);
  3432. adoptedStyleSheetData.styleIds.push(styleId);
  3433. }
  3434. if (styles.length > 0)
  3435. adoptedStyleSheetData.styles = styles;
  3436. this.adoptedStyleSheetCb(adoptedStyleSheetData);
  3437. }
  3438. reset() {
  3439. this.styleMirror.reset();
  3440. this.trackedLinkElements = new WeakSet();
  3441. }
  3442. trackStylesheetInLinkElement(linkEl) {
  3443. }
  3444. }
  3445. class ProcessedNodeManager {
  3446. constructor() {
  3447. this.nodeMap = new WeakMap();
  3448. this.loop = true;
  3449. this.periodicallyClear();
  3450. }
  3451. periodicallyClear() {
  3452. onRequestAnimationFrame(() => {
  3453. this.clear();
  3454. if (this.loop)
  3455. this.periodicallyClear();
  3456. });
  3457. }
  3458. inOtherBuffer(node, thisBuffer) {
  3459. const buffers = this.nodeMap.get(node);
  3460. return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer));
  3461. }
  3462. add(node, buffer) {
  3463. this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer));
  3464. }
  3465. clear() {
  3466. this.nodeMap = new WeakMap();
  3467. }
  3468. destroy() {
  3469. this.loop = false;
  3470. }
  3471. }
  3472. function wrapEvent(e) {
  3473. const eWithTime = e;
  3474. eWithTime.timestamp = nowTimestamp();
  3475. return eWithTime;
  3476. }
  3477. let _takeFullSnapshot;
  3478. const mirror = createMirror();
  3479. function record(options = {}) {
  3480. 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'
  3481. ? options.recordAfter
  3482. : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, onMutation, getCanvasManager, } = options;
  3483. registerErrorHandler(errorHandler);
  3484. const inEmittingFrame = recordCrossOriginIframes
  3485. ? window.parent === window
  3486. : true;
  3487. let passEmitsToParent = false;
  3488. if (!inEmittingFrame) {
  3489. try {
  3490. if (window.parent.document) {
  3491. passEmitsToParent = false;
  3492. }
  3493. }
  3494. catch (e) {
  3495. passEmitsToParent = true;
  3496. }
  3497. }
  3498. if (inEmittingFrame && !emit) {
  3499. throw new Error('emit function is required');
  3500. }
  3501. if (mousemoveWait !== undefined && sampling.mousemove === undefined) {
  3502. sampling.mousemove = mousemoveWait;
  3503. }
  3504. mirror.reset();
  3505. const maskInputOptions = maskAllInputs === true
  3506. ? {
  3507. color: true,
  3508. date: true,
  3509. 'datetime-local': true,
  3510. email: true,
  3511. month: true,
  3512. number: true,
  3513. range: true,
  3514. search: true,
  3515. tel: true,
  3516. text: true,
  3517. time: true,
  3518. url: true,
  3519. week: true,
  3520. textarea: true,
  3521. select: true,
  3522. radio: true,
  3523. checkbox: true,
  3524. }
  3525. : _maskInputOptions !== undefined
  3526. ? _maskInputOptions
  3527. : {};
  3528. const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all'
  3529. ? {
  3530. script: true,
  3531. comment: true,
  3532. headFavicon: true,
  3533. headWhitespace: true,
  3534. headMetaSocial: true,
  3535. headMetaRobots: true,
  3536. headMetaHttpEquiv: true,
  3537. headMetaVerification: true,
  3538. headMetaAuthorship: _slimDOMOptions === 'all',
  3539. headMetaDescKeywords: _slimDOMOptions === 'all',
  3540. }
  3541. : _slimDOMOptions
  3542. ? _slimDOMOptions
  3543. : {};
  3544. polyfill();
  3545. let lastFullSnapshotEvent;
  3546. let incrementalSnapshotCount = 0;
  3547. const eventProcessor = (e) => {
  3548. for (const plugin of plugins || []) {
  3549. if (plugin.eventProcessor) {
  3550. e = plugin.eventProcessor(e);
  3551. }
  3552. }
  3553. if (packFn &&
  3554. !passEmitsToParent) {
  3555. e = packFn(e);
  3556. }
  3557. return e;
  3558. };
  3559. const wrappedEmit = (e, isCheckout) => {
  3560. if (_optionalChain([mutationBuffers, 'access', _ => _[0], 'optionalAccess', _2 => _2.isFrozen, 'call', _3 => _3()]) &&
  3561. e.type !== EventType.FullSnapshot &&
  3562. !(e.type === EventType.IncrementalSnapshot &&
  3563. e.data.source === IncrementalSource.Mutation)) {
  3564. mutationBuffers.forEach((buf) => buf.unfreeze());
  3565. }
  3566. if (inEmittingFrame) {
  3567. _optionalChain([emit, 'optionalCall', _4 => _4(eventProcessor(e), isCheckout)]);
  3568. }
  3569. else if (passEmitsToParent) {
  3570. const message = {
  3571. type: 'rrweb',
  3572. event: eventProcessor(e),
  3573. origin: window.location.origin,
  3574. isCheckout,
  3575. };
  3576. window.parent.postMessage(message, '*');
  3577. }
  3578. if (e.type === EventType.FullSnapshot) {
  3579. lastFullSnapshotEvent = e;
  3580. incrementalSnapshotCount = 0;
  3581. }
  3582. else if (e.type === EventType.IncrementalSnapshot) {
  3583. if (e.data.source === IncrementalSource.Mutation &&
  3584. e.data.isAttachIframe) {
  3585. return;
  3586. }
  3587. incrementalSnapshotCount++;
  3588. const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth;
  3589. const exceedTime = checkoutEveryNms &&
  3590. e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms;
  3591. if (exceedCount || exceedTime) {
  3592. takeFullSnapshot(true);
  3593. }
  3594. }
  3595. };
  3596. const wrappedMutationEmit = (m) => {
  3597. wrappedEmit(wrapEvent({
  3598. type: EventType.IncrementalSnapshot,
  3599. data: {
  3600. source: IncrementalSource.Mutation,
  3601. ...m,
  3602. },
  3603. }));
  3604. };
  3605. const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({
  3606. type: EventType.IncrementalSnapshot,
  3607. data: {
  3608. source: IncrementalSource.Scroll,
  3609. ...p,
  3610. },
  3611. }));
  3612. const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({
  3613. type: EventType.IncrementalSnapshot,
  3614. data: {
  3615. source: IncrementalSource.CanvasMutation,
  3616. ...p,
  3617. },
  3618. }));
  3619. const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({
  3620. type: EventType.IncrementalSnapshot,
  3621. data: {
  3622. source: IncrementalSource.AdoptedStyleSheet,
  3623. ...a,
  3624. },
  3625. }));
  3626. const stylesheetManager = new StylesheetManager({
  3627. mutationCb: wrappedMutationEmit,
  3628. adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit,
  3629. });
  3630. const iframeManager = typeof __RRWEB_EXCLUDE_IFRAME__ === 'boolean' && __RRWEB_EXCLUDE_IFRAME__
  3631. ? new IframeManagerNoop()
  3632. : new IframeManager({
  3633. mirror,
  3634. mutationCb: wrappedMutationEmit,
  3635. stylesheetManager: stylesheetManager,
  3636. recordCrossOriginIframes,
  3637. wrappedEmit,
  3638. });
  3639. for (const plugin of plugins || []) {
  3640. if (plugin.getMirror)
  3641. plugin.getMirror({
  3642. nodeMirror: mirror,
  3643. crossOriginIframeMirror: iframeManager.crossOriginIframeMirror,
  3644. crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror,
  3645. });
  3646. }
  3647. const processedNodeManager = new ProcessedNodeManager();
  3648. const canvasManager = _getCanvasManager(getCanvasManager, {
  3649. mirror,
  3650. win: window,
  3651. mutationCb: (p) => wrappedEmit(wrapEvent({
  3652. type: EventType.IncrementalSnapshot,
  3653. data: {
  3654. source: IncrementalSource.CanvasMutation,
  3655. ...p,
  3656. },
  3657. })),
  3658. recordCanvas,
  3659. blockClass,
  3660. blockSelector,
  3661. unblockSelector,
  3662. sampling: sampling['canvas'],
  3663. dataURLOptions,
  3664. errorHandler,
  3665. });
  3666. const shadowDomManager = typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' &&
  3667. __RRWEB_EXCLUDE_SHADOW_DOM__
  3668. ? new ShadowDomManagerNoop()
  3669. : new ShadowDomManager({
  3670. mutationCb: wrappedMutationEmit,
  3671. scrollCb: wrappedScrollEmit,
  3672. bypassOptions: {
  3673. onMutation,
  3674. blockClass,
  3675. blockSelector,
  3676. unblockSelector,
  3677. maskAllText,
  3678. maskTextClass,
  3679. unmaskTextClass,
  3680. maskTextSelector,
  3681. unmaskTextSelector,
  3682. inlineStylesheet,
  3683. maskInputOptions,
  3684. dataURLOptions,
  3685. maskAttributeFn,
  3686. maskTextFn,
  3687. maskInputFn,
  3688. recordCanvas,
  3689. inlineImages,
  3690. sampling,
  3691. slimDOMOptions,
  3692. iframeManager,
  3693. stylesheetManager,
  3694. canvasManager,
  3695. keepIframeSrcFn,
  3696. processedNodeManager,
  3697. },
  3698. mirror,
  3699. });
  3700. const takeFullSnapshot = (isCheckout = false) => {
  3701. wrappedEmit(wrapEvent({
  3702. type: EventType.Meta,
  3703. data: {
  3704. href: window.location.href,
  3705. width: getWindowWidth(),
  3706. height: getWindowHeight(),
  3707. },
  3708. }), isCheckout);
  3709. stylesheetManager.reset();
  3710. shadowDomManager.init();
  3711. mutationBuffers.forEach((buf) => buf.lock());
  3712. const node = snapshot(document, {
  3713. mirror,
  3714. blockClass,
  3715. blockSelector,
  3716. unblockSelector,
  3717. maskAllText,
  3718. maskTextClass,
  3719. unmaskTextClass,
  3720. maskTextSelector,
  3721. unmaskTextSelector,
  3722. inlineStylesheet,
  3723. maskAllInputs: maskInputOptions,
  3724. maskAttributeFn,
  3725. maskInputFn,
  3726. maskTextFn,
  3727. slimDOM: slimDOMOptions,
  3728. dataURLOptions,
  3729. recordCanvas,
  3730. inlineImages,
  3731. onSerialize: (n) => {
  3732. if (isSerializedIframe(n, mirror)) {
  3733. iframeManager.addIframe(n);
  3734. }
  3735. if (isSerializedStylesheet(n, mirror)) {
  3736. stylesheetManager.trackLinkElement(n);
  3737. }
  3738. if (hasShadowRoot(n)) {
  3739. shadowDomManager.addShadowRoot(n.shadowRoot, document);
  3740. }
  3741. },
  3742. onIframeLoad: (iframe, childSn) => {
  3743. iframeManager.attachIframe(iframe, childSn);
  3744. shadowDomManager.observeAttachShadow(iframe);
  3745. },
  3746. onStylesheetLoad: (linkEl, childSn) => {
  3747. stylesheetManager.attachLinkElement(linkEl, childSn);
  3748. },
  3749. keepIframeSrcFn,
  3750. });
  3751. if (!node) {
  3752. return console.warn('Failed to snapshot the document');
  3753. }
  3754. wrappedEmit(wrapEvent({
  3755. type: EventType.FullSnapshot,
  3756. data: {
  3757. node,
  3758. initialOffset: getWindowScroll(window),
  3759. },
  3760. }));
  3761. mutationBuffers.forEach((buf) => buf.unlock());
  3762. if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0)
  3763. stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document));
  3764. };
  3765. _takeFullSnapshot = takeFullSnapshot;
  3766. try {
  3767. const handlers = [];
  3768. const observe = (doc) => {
  3769. return callbackWrapper(initObservers)({
  3770. onMutation,
  3771. mutationCb: wrappedMutationEmit,
  3772. mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({
  3773. type: EventType.IncrementalSnapshot,
  3774. data: {
  3775. source,
  3776. positions,
  3777. },
  3778. })),
  3779. mouseInteractionCb: (d) => wrappedEmit(wrapEvent({
  3780. type: EventType.IncrementalSnapshot,
  3781. data: {
  3782. source: IncrementalSource.MouseInteraction,
  3783. ...d,
  3784. },
  3785. })),
  3786. scrollCb: wrappedScrollEmit,
  3787. viewportResizeCb: (d) => wrappedEmit(wrapEvent({
  3788. type: EventType.IncrementalSnapshot,
  3789. data: {
  3790. source: IncrementalSource.ViewportResize,
  3791. ...d,
  3792. },
  3793. })),
  3794. inputCb: (v) => wrappedEmit(wrapEvent({
  3795. type: EventType.IncrementalSnapshot,
  3796. data: {
  3797. source: IncrementalSource.Input,
  3798. ...v,
  3799. },
  3800. })),
  3801. mediaInteractionCb: (p) => wrappedEmit(wrapEvent({
  3802. type: EventType.IncrementalSnapshot,
  3803. data: {
  3804. source: IncrementalSource.MediaInteraction,
  3805. ...p,
  3806. },
  3807. })),
  3808. styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({
  3809. type: EventType.IncrementalSnapshot,
  3810. data: {
  3811. source: IncrementalSource.StyleSheetRule,
  3812. ...r,
  3813. },
  3814. })),
  3815. styleDeclarationCb: (r) => wrappedEmit(wrapEvent({
  3816. type: EventType.IncrementalSnapshot,
  3817. data: {
  3818. source: IncrementalSource.StyleDeclaration,
  3819. ...r,
  3820. },
  3821. })),
  3822. canvasMutationCb: wrappedCanvasMutationEmit,
  3823. fontCb: (p) => wrappedEmit(wrapEvent({
  3824. type: EventType.IncrementalSnapshot,
  3825. data: {
  3826. source: IncrementalSource.Font,
  3827. ...p,
  3828. },
  3829. })),
  3830. selectionCb: (p) => {
  3831. wrappedEmit(wrapEvent({
  3832. type: EventType.IncrementalSnapshot,
  3833. data: {
  3834. source: IncrementalSource.Selection,
  3835. ...p,
  3836. },
  3837. }));
  3838. },
  3839. customElementCb: (c) => {
  3840. wrappedEmit(wrapEvent({
  3841. type: EventType.IncrementalSnapshot,
  3842. data: {
  3843. source: IncrementalSource.CustomElement,
  3844. ...c,
  3845. },
  3846. }));
  3847. },
  3848. blockClass,
  3849. ignoreClass,
  3850. ignoreSelector,
  3851. maskAllText,
  3852. maskTextClass,
  3853. unmaskTextClass,
  3854. maskTextSelector,
  3855. unmaskTextSelector,
  3856. maskInputOptions,
  3857. inlineStylesheet,
  3858. sampling,
  3859. recordCanvas,
  3860. inlineImages,
  3861. userTriggeredOnInput,
  3862. collectFonts,
  3863. doc,
  3864. maskAttributeFn,
  3865. maskInputFn,
  3866. maskTextFn,
  3867. keepIframeSrcFn,
  3868. blockSelector,
  3869. unblockSelector,
  3870. slimDOMOptions,
  3871. dataURLOptions,
  3872. mirror,
  3873. iframeManager,
  3874. stylesheetManager,
  3875. shadowDomManager,
  3876. processedNodeManager,
  3877. canvasManager,
  3878. ignoreCSSAttributes,
  3879. plugins: _optionalChain([plugins
  3880. , 'optionalAccess', _5 => _5.filter, 'call', _6 => _6((p) => p.observer)
  3881. , 'optionalAccess', _7 => _7.map, 'call', _8 => _8((p) => ({
  3882. observer: p.observer,
  3883. options: p.options,
  3884. callback: (payload) => wrappedEmit(wrapEvent({
  3885. type: EventType.Plugin,
  3886. data: {
  3887. plugin: p.name,
  3888. payload,
  3889. },
  3890. })),
  3891. }))]) || [],
  3892. }, {});
  3893. };
  3894. iframeManager.addLoadListener((iframeEl) => {
  3895. try {
  3896. handlers.push(observe(iframeEl.contentDocument));
  3897. }
  3898. catch (error) {
  3899. console.warn(error);
  3900. }
  3901. });
  3902. const init = () => {
  3903. takeFullSnapshot();
  3904. handlers.push(observe(document));
  3905. };
  3906. if (document.readyState === 'interactive' ||
  3907. document.readyState === 'complete') {
  3908. init();
  3909. }
  3910. else {
  3911. handlers.push(on('DOMContentLoaded', () => {
  3912. wrappedEmit(wrapEvent({
  3913. type: EventType.DomContentLoaded,
  3914. data: {},
  3915. }));
  3916. if (recordAfter === 'DOMContentLoaded')
  3917. init();
  3918. }));
  3919. handlers.push(on('load', () => {
  3920. wrappedEmit(wrapEvent({
  3921. type: EventType.Load,
  3922. data: {},
  3923. }));
  3924. if (recordAfter === 'load')
  3925. init();
  3926. }, window));
  3927. }
  3928. return () => {
  3929. handlers.forEach((h) => h());
  3930. processedNodeManager.destroy();
  3931. _takeFullSnapshot = undefined;
  3932. unregisterErrorHandler();
  3933. };
  3934. }
  3935. catch (error) {
  3936. console.warn(error);
  3937. }
  3938. }
  3939. function takeFullSnapshot(isCheckout) {
  3940. if (!_takeFullSnapshot) {
  3941. throw new Error('please take full snapshot after start recording');
  3942. }
  3943. _takeFullSnapshot(isCheckout);
  3944. }
  3945. record.mirror = mirror;
  3946. record.takeFullSnapshot = takeFullSnapshot;
  3947. function _getCanvasManager(getCanvasManagerFn, options) {
  3948. try {
  3949. return getCanvasManagerFn
  3950. ? getCanvasManagerFn(options)
  3951. : new CanvasManagerNoop();
  3952. }
  3953. catch (e2) {
  3954. console.warn('Unable to initialize CanvasManager');
  3955. return new CanvasManagerNoop();
  3956. }
  3957. }
  3958. const ReplayEventTypeIncrementalSnapshot = 3;
  3959. const ReplayEventTypeCustom = 5;
  3960. /**
  3961. * Converts a timestamp to ms, if it was in s, or keeps it as ms.
  3962. */
  3963. function timestampToMs(timestamp) {
  3964. const isMs = timestamp > 9999999999;
  3965. return isMs ? timestamp : timestamp * 1000;
  3966. }
  3967. /**
  3968. * Converts a timestamp to s, if it was in ms, or keeps it as s.
  3969. */
  3970. function timestampToS(timestamp) {
  3971. const isMs = timestamp > 9999999999;
  3972. return isMs ? timestamp / 1000 : timestamp;
  3973. }
  3974. /**
  3975. * Add a breadcrumb event to replay.
  3976. */
  3977. function addBreadcrumbEvent(replay, breadcrumb) {
  3978. if (breadcrumb.category === 'sentry.transaction') {
  3979. return;
  3980. }
  3981. if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
  3982. replay.triggerUserActivity();
  3983. } else {
  3984. replay.checkAndHandleExpiredSession();
  3985. }
  3986. replay.addUpdate(() => {
  3987. // This should never reject
  3988. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  3989. replay.throttledAddEvent({
  3990. type: EventType.Custom,
  3991. // TODO: We were converting from ms to seconds for breadcrumbs, spans,
  3992. // but maybe we should just keep them as milliseconds
  3993. timestamp: (breadcrumb.timestamp || 0) * 1000,
  3994. data: {
  3995. tag: 'breadcrumb',
  3996. // normalize to max. 10 depth and 1_000 properties per object
  3997. payload: normalize(breadcrumb, 10, 1000),
  3998. },
  3999. });
  4000. // Do not flush after console log messages
  4001. return breadcrumb.category === 'console';
  4002. });
  4003. }
  4004. const INTERACTIVE_SELECTOR = 'button,a';
  4005. /** Get the closest interactive parent element, or else return the given element. */
  4006. function getClosestInteractive(element) {
  4007. const closestInteractive = element.closest(INTERACTIVE_SELECTOR);
  4008. return closestInteractive || element;
  4009. }
  4010. /**
  4011. * For clicks, we check if the target is inside of a button or link
  4012. * If so, we use this as the target instead
  4013. * This is useful because if you click on the image in <button><img></button>,
  4014. * The target will be the image, not the button, which we don't want here
  4015. */
  4016. function getClickTargetNode(event) {
  4017. const target = getTargetNode(event);
  4018. if (!target || !(target instanceof Element)) {
  4019. return target;
  4020. }
  4021. return getClosestInteractive(target);
  4022. }
  4023. /** Get the event target node. */
  4024. function getTargetNode(event) {
  4025. if (isEventWithTarget(event)) {
  4026. return event.target ;
  4027. }
  4028. return event;
  4029. }
  4030. function isEventWithTarget(event) {
  4031. return typeof event === 'object' && !!event && 'target' in event;
  4032. }
  4033. let handlers;
  4034. /**
  4035. * Register a handler to be called when `window.open()` is called.
  4036. * Returns a cleanup function.
  4037. */
  4038. function onWindowOpen(cb) {
  4039. // Ensure to only register this once
  4040. if (!handlers) {
  4041. handlers = [];
  4042. monkeyPatchWindowOpen();
  4043. }
  4044. handlers.push(cb);
  4045. return () => {
  4046. const pos = handlers ? handlers.indexOf(cb) : -1;
  4047. if (pos > -1) {
  4048. (handlers ).splice(pos, 1);
  4049. }
  4050. };
  4051. }
  4052. function monkeyPatchWindowOpen() {
  4053. fill(WINDOW, 'open', function (originalWindowOpen) {
  4054. return function (...args) {
  4055. if (handlers) {
  4056. try {
  4057. handlers.forEach(handler => handler());
  4058. } catch (e) {
  4059. // ignore errors in here
  4060. }
  4061. }
  4062. return originalWindowOpen.apply(WINDOW, args);
  4063. };
  4064. });
  4065. }
  4066. /** Handle a click. */
  4067. function handleClick(clickDetector, clickBreadcrumb, node) {
  4068. clickDetector.handleClick(clickBreadcrumb, node);
  4069. }
  4070. /** A click detector class that can be used to detect slow or rage clicks on elements. */
  4071. class ClickDetector {
  4072. // protected for testing
  4073. constructor(
  4074. replay,
  4075. slowClickConfig,
  4076. // Just for easier testing
  4077. _addBreadcrumbEvent = addBreadcrumbEvent,
  4078. ) {
  4079. this._lastMutation = 0;
  4080. this._lastScroll = 0;
  4081. this._clicks = [];
  4082. // We want everything in s, but options are in ms
  4083. this._timeout = slowClickConfig.timeout / 1000;
  4084. this._threshold = slowClickConfig.threshold / 1000;
  4085. this._scollTimeout = slowClickConfig.scrollTimeout / 1000;
  4086. this._replay = replay;
  4087. this._ignoreSelector = slowClickConfig.ignoreSelector;
  4088. this._addBreadcrumbEvent = _addBreadcrumbEvent;
  4089. }
  4090. /** Register click detection handlers on mutation or scroll. */
  4091. addListeners() {
  4092. const cleanupWindowOpen = onWindowOpen(() => {
  4093. // Treat window.open as mutation
  4094. this._lastMutation = nowInSeconds();
  4095. });
  4096. this._teardown = () => {
  4097. cleanupWindowOpen();
  4098. this._clicks = [];
  4099. this._lastMutation = 0;
  4100. this._lastScroll = 0;
  4101. };
  4102. }
  4103. /** Clean up listeners. */
  4104. removeListeners() {
  4105. if (this._teardown) {
  4106. this._teardown();
  4107. }
  4108. if (this._checkClickTimeout) {
  4109. clearTimeout(this._checkClickTimeout);
  4110. }
  4111. }
  4112. /** @inheritDoc */
  4113. handleClick(breadcrumb, node) {
  4114. if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) {
  4115. return;
  4116. }
  4117. const newClick = {
  4118. timestamp: timestampToS(breadcrumb.timestamp),
  4119. clickBreadcrumb: breadcrumb,
  4120. // Set this to 0 so we know it originates from the click breadcrumb
  4121. clickCount: 0,
  4122. node,
  4123. };
  4124. // If there was a click in the last 1s on the same element, ignore it - only keep a single reference per second
  4125. if (
  4126. this._clicks.some(click => click.node === newClick.node && Math.abs(click.timestamp - newClick.timestamp) < 1)
  4127. ) {
  4128. return;
  4129. }
  4130. this._clicks.push(newClick);
  4131. // If this is the first new click, set a timeout to check for multi clicks
  4132. if (this._clicks.length === 1) {
  4133. this._scheduleCheckClicks();
  4134. }
  4135. }
  4136. /** @inheritDoc */
  4137. registerMutation(timestamp = Date.now()) {
  4138. this._lastMutation = timestampToS(timestamp);
  4139. }
  4140. /** @inheritDoc */
  4141. registerScroll(timestamp = Date.now()) {
  4142. this._lastScroll = timestampToS(timestamp);
  4143. }
  4144. /** @inheritDoc */
  4145. registerClick(element) {
  4146. const node = getClosestInteractive(element);
  4147. this._handleMultiClick(node );
  4148. }
  4149. /** Count multiple clicks on elements. */
  4150. _handleMultiClick(node) {
  4151. this._getClicks(node).forEach(click => {
  4152. click.clickCount++;
  4153. });
  4154. }
  4155. /** Get all pending clicks for a given node. */
  4156. _getClicks(node) {
  4157. return this._clicks.filter(click => click.node === node);
  4158. }
  4159. /** Check the clicks that happened. */
  4160. _checkClicks() {
  4161. const timedOutClicks = [];
  4162. const now = nowInSeconds();
  4163. this._clicks.forEach(click => {
  4164. if (!click.mutationAfter && this._lastMutation) {
  4165. click.mutationAfter = click.timestamp <= this._lastMutation ? this._lastMutation - click.timestamp : undefined;
  4166. }
  4167. if (!click.scrollAfter && this._lastScroll) {
  4168. click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined;
  4169. }
  4170. // All of these are in seconds!
  4171. if (click.timestamp + this._timeout <= now) {
  4172. timedOutClicks.push(click);
  4173. }
  4174. });
  4175. // Remove "old" clicks
  4176. for (const click of timedOutClicks) {
  4177. const pos = this._clicks.indexOf(click);
  4178. if (pos > -1) {
  4179. this._generateBreadcrumbs(click);
  4180. this._clicks.splice(pos, 1);
  4181. }
  4182. }
  4183. // Trigger new check, unless no clicks left
  4184. if (this._clicks.length) {
  4185. this._scheduleCheckClicks();
  4186. }
  4187. }
  4188. /** Generate matching breadcrumb(s) for the click. */
  4189. _generateBreadcrumbs(click) {
  4190. const replay = this._replay;
  4191. const hadScroll = click.scrollAfter && click.scrollAfter <= this._scollTimeout;
  4192. const hadMutation = click.mutationAfter && click.mutationAfter <= this._threshold;
  4193. const isSlowClick = !hadScroll && !hadMutation;
  4194. const { clickCount, clickBreadcrumb } = click;
  4195. // Slow click
  4196. if (isSlowClick) {
  4197. // If `mutationAfter` is set, it means a mutation happened after the threshold, but before the timeout
  4198. // If not, it means we just timed out without scroll & mutation
  4199. const timeAfterClickMs = Math.min(click.mutationAfter || this._timeout, this._timeout) * 1000;
  4200. const endReason = timeAfterClickMs < this._timeout * 1000 ? 'mutation' : 'timeout';
  4201. const breadcrumb = {
  4202. type: 'default',
  4203. message: clickBreadcrumb.message,
  4204. timestamp: clickBreadcrumb.timestamp,
  4205. category: 'ui.slowClickDetected',
  4206. data: {
  4207. ...clickBreadcrumb.data,
  4208. url: WINDOW.location.href,
  4209. route: replay.getCurrentRoute(),
  4210. timeAfterClickMs,
  4211. endReason,
  4212. // If clickCount === 0, it means multiClick was not correctly captured here
  4213. // - we still want to send 1 in this case
  4214. clickCount: clickCount || 1,
  4215. },
  4216. };
  4217. this._addBreadcrumbEvent(replay, breadcrumb);
  4218. return;
  4219. }
  4220. // Multi click
  4221. if (clickCount > 1) {
  4222. const breadcrumb = {
  4223. type: 'default',
  4224. message: clickBreadcrumb.message,
  4225. timestamp: clickBreadcrumb.timestamp,
  4226. category: 'ui.multiClick',
  4227. data: {
  4228. ...clickBreadcrumb.data,
  4229. url: WINDOW.location.href,
  4230. route: replay.getCurrentRoute(),
  4231. clickCount,
  4232. metric: true,
  4233. },
  4234. };
  4235. this._addBreadcrumbEvent(replay, breadcrumb);
  4236. }
  4237. }
  4238. /** Schedule to check current clicks. */
  4239. _scheduleCheckClicks() {
  4240. if (this._checkClickTimeout) {
  4241. clearTimeout(this._checkClickTimeout);
  4242. }
  4243. this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000);
  4244. }
  4245. }
  4246. const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT'];
  4247. /** exported for tests only */
  4248. function ignoreElement(node, ignoreSelector) {
  4249. if (!SLOW_CLICK_TAGS.includes(node.tagName)) {
  4250. return true;
  4251. }
  4252. // If <input> tag, we only want to consider input[type='submit'] & input[type='button']
  4253. if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) {
  4254. return true;
  4255. }
  4256. // If <a> tag, detect special variants that may not lead to an action
  4257. // If target !== _self, we may open the link somewhere else, which would lead to no action
  4258. // Also, when downloading a file, we may not leave the page, but still not trigger an action
  4259. if (
  4260. node.tagName === 'A' &&
  4261. (node.hasAttribute('download') || (node.hasAttribute('target') && node.getAttribute('target') !== '_self'))
  4262. ) {
  4263. return true;
  4264. }
  4265. if (ignoreSelector && node.matches(ignoreSelector)) {
  4266. return true;
  4267. }
  4268. return false;
  4269. }
  4270. function isClickBreadcrumb(breadcrumb) {
  4271. return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp);
  4272. }
  4273. // This is good enough for us, and is easier to test/mock than `timestampInSeconds`
  4274. function nowInSeconds() {
  4275. return Date.now() / 1000;
  4276. }
  4277. /** Update the click detector based on a recording event of rrweb. */
  4278. function updateClickDetectorForRecordingEvent(clickDetector, event) {
  4279. try {
  4280. // note: We only consider incremental snapshots here
  4281. // This means that any full snapshot is ignored for mutation detection - the reason is that we simply cannot know if a mutation happened here.
  4282. // E.g. think that we are buffering, an error happens and we take a full snapshot because we switched to session mode -
  4283. // in this scenario, we would not know if a dead click happened because of the error, which is a key dead click scenario.
  4284. // Instead, by ignoring full snapshots, we have the risk that we generate a false positive
  4285. // (if a mutation _did_ happen but was "swallowed" by the full snapshot)
  4286. // But this should be more unlikely as we'd generally capture the incremental snapshot right away
  4287. if (!isIncrementalEvent(event)) {
  4288. return;
  4289. }
  4290. const { source } = event.data;
  4291. if (source === IncrementalSource.Mutation) {
  4292. clickDetector.registerMutation(event.timestamp);
  4293. }
  4294. if (source === IncrementalSource.Scroll) {
  4295. clickDetector.registerScroll(event.timestamp);
  4296. }
  4297. if (isIncrementalMouseInteraction(event)) {
  4298. const { type, id } = event.data;
  4299. const node = record.mirror.getNode(id);
  4300. if (node instanceof HTMLElement && type === MouseInteractions.Click) {
  4301. clickDetector.registerClick(node);
  4302. }
  4303. }
  4304. } catch (e) {
  4305. // ignore errors here, e.g. if accessing something that does not exist
  4306. }
  4307. }
  4308. function isIncrementalEvent(event) {
  4309. return event.type === ReplayEventTypeIncrementalSnapshot;
  4310. }
  4311. function isIncrementalMouseInteraction(
  4312. event,
  4313. ) {
  4314. return event.data.source === IncrementalSource.MouseInteraction;
  4315. }
  4316. /**
  4317. * Create a breadcrumb for a replay.
  4318. */
  4319. function createBreadcrumb(
  4320. breadcrumb,
  4321. ) {
  4322. return {
  4323. timestamp: Date.now() / 1000,
  4324. type: 'default',
  4325. ...breadcrumb,
  4326. };
  4327. }
  4328. var NodeType;
  4329. (function (NodeType) {
  4330. NodeType[NodeType["Document"] = 0] = "Document";
  4331. NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
  4332. NodeType[NodeType["Element"] = 2] = "Element";
  4333. NodeType[NodeType["Text"] = 3] = "Text";
  4334. NodeType[NodeType["CDATA"] = 4] = "CDATA";
  4335. NodeType[NodeType["Comment"] = 5] = "Comment";
  4336. })(NodeType || (NodeType = {}));
  4337. // Note that these are the serialized attributes and not attributes directly on
  4338. // the DOM Node. Attributes we are interested in:
  4339. const ATTRIBUTES_TO_RECORD = new Set([
  4340. 'id',
  4341. 'class',
  4342. 'aria-label',
  4343. 'role',
  4344. 'name',
  4345. 'alt',
  4346. 'title',
  4347. 'data-test-id',
  4348. 'data-testid',
  4349. 'disabled',
  4350. 'aria-disabled',
  4351. 'data-sentry-component',
  4352. ]);
  4353. /**
  4354. * Inclusion list of attributes that we want to record from the DOM element
  4355. */
  4356. function getAttributesToRecord(attributes) {
  4357. const obj = {};
  4358. for (const key in attributes) {
  4359. if (ATTRIBUTES_TO_RECORD.has(key)) {
  4360. let normalizedKey = key;
  4361. if (key === 'data-testid' || key === 'data-test-id') {
  4362. normalizedKey = 'testId';
  4363. }
  4364. obj[normalizedKey] = attributes[key];
  4365. }
  4366. }
  4367. return obj;
  4368. }
  4369. const handleDomListener = (
  4370. replay,
  4371. ) => {
  4372. return (handlerData) => {
  4373. if (!replay.isEnabled()) {
  4374. return;
  4375. }
  4376. const result = handleDom(handlerData);
  4377. if (!result) {
  4378. return;
  4379. }
  4380. const isClick = handlerData.name === 'click';
  4381. const event = isClick ? (handlerData.event ) : undefined;
  4382. // Ignore clicks if ctrl/alt/meta/shift keys are held down as they alter behavior of clicks (e.g. open in new tab)
  4383. if (
  4384. isClick &&
  4385. replay.clickDetector &&
  4386. event &&
  4387. event.target &&
  4388. !event.altKey &&
  4389. !event.metaKey &&
  4390. !event.ctrlKey &&
  4391. !event.shiftKey
  4392. ) {
  4393. handleClick(
  4394. replay.clickDetector,
  4395. result ,
  4396. getClickTargetNode(handlerData.event ) ,
  4397. );
  4398. }
  4399. addBreadcrumbEvent(replay, result);
  4400. };
  4401. };
  4402. /** Get the base DOM breadcrumb. */
  4403. function getBaseDomBreadcrumb(target, message) {
  4404. const nodeId = record.mirror.getId(target);
  4405. const node = nodeId && record.mirror.getNode(nodeId);
  4406. const meta = node && record.mirror.getMeta(node);
  4407. const element = meta && isElement(meta) ? meta : null;
  4408. return {
  4409. message,
  4410. data: element
  4411. ? {
  4412. nodeId,
  4413. node: {
  4414. id: nodeId,
  4415. tagName: element.tagName,
  4416. textContent: Array.from(element.childNodes)
  4417. .map((node) => node.type === NodeType.Text && node.textContent)
  4418. .filter(Boolean) // filter out empty values
  4419. .map(text => (text ).trim())
  4420. .join(''),
  4421. attributes: getAttributesToRecord(element.attributes),
  4422. },
  4423. }
  4424. : {},
  4425. };
  4426. }
  4427. /**
  4428. * An event handler to react to DOM events.
  4429. * Exported for tests.
  4430. */
  4431. function handleDom(handlerData) {
  4432. const { target, message } = getDomTarget(handlerData);
  4433. return createBreadcrumb({
  4434. category: `ui.${handlerData.name}`,
  4435. ...getBaseDomBreadcrumb(target, message),
  4436. });
  4437. }
  4438. function getDomTarget(handlerData) {
  4439. const isClick = handlerData.name === 'click';
  4440. let message;
  4441. let target = null;
  4442. // Accessing event.target can throw (see getsentry/raven-js#838, #768)
  4443. try {
  4444. target = isClick ? getClickTargetNode(handlerData.event ) : getTargetNode(handlerData.event );
  4445. message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
  4446. } catch (e) {
  4447. message = '<unknown>';
  4448. }
  4449. return { target, message };
  4450. }
  4451. function isElement(node) {
  4452. return node.type === NodeType.Element;
  4453. }
  4454. /** Handle keyboard events & create breadcrumbs. */
  4455. function handleKeyboardEvent(replay, event) {
  4456. if (!replay.isEnabled()) {
  4457. return;
  4458. }
  4459. // Update user activity, but do not restart recording as it can create
  4460. // noisy/low-value replays (e.g. user comes back from idle, hits alt-tab, new
  4461. // session with a single "keydown" breadcrumb is created)
  4462. replay.updateUserActivity();
  4463. const breadcrumb = getKeyboardBreadcrumb(event);
  4464. if (!breadcrumb) {
  4465. return;
  4466. }
  4467. addBreadcrumbEvent(replay, breadcrumb);
  4468. }
  4469. /** exported only for tests */
  4470. function getKeyboardBreadcrumb(event) {
  4471. const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event;
  4472. // never capture for input fields
  4473. if (!target || isInputElement(target ) || !key) {
  4474. return null;
  4475. }
  4476. // Note: We do not consider shift here, as that means "uppercase"
  4477. const hasModifierKey = metaKey || ctrlKey || altKey;
  4478. const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length
  4479. // Do not capture breadcrumb if only a word key is pressed
  4480. // This could leak e.g. user input
  4481. if (!hasModifierKey && isCharacterKey) {
  4482. return null;
  4483. }
  4484. const message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
  4485. const baseBreadcrumb = getBaseDomBreadcrumb(target , message);
  4486. return createBreadcrumb({
  4487. category: 'ui.keyDown',
  4488. message,
  4489. data: {
  4490. ...baseBreadcrumb.data,
  4491. metaKey,
  4492. shiftKey,
  4493. ctrlKey,
  4494. altKey,
  4495. key,
  4496. },
  4497. });
  4498. }
  4499. function isInputElement(target) {
  4500. return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
  4501. }
  4502. // Map entryType -> function to normalize data for event
  4503. const ENTRY_TYPES
  4504. = {
  4505. // @ts-expect-error TODO: entry type does not fit the create* functions entry type
  4506. resource: createResourceEntry,
  4507. paint: createPaintEntry,
  4508. // @ts-expect-error TODO: entry type does not fit the create* functions entry type
  4509. navigation: createNavigationEntry,
  4510. };
  4511. /**
  4512. * Create replay performance entries from the browser performance entries.
  4513. */
  4514. function createPerformanceEntries(
  4515. entries,
  4516. ) {
  4517. return entries.map(createPerformanceEntry).filter(Boolean) ;
  4518. }
  4519. function createPerformanceEntry(entry) {
  4520. if (!ENTRY_TYPES[entry.entryType]) {
  4521. return null;
  4522. }
  4523. return ENTRY_TYPES[entry.entryType](entry);
  4524. }
  4525. function getAbsoluteTime(time) {
  4526. // browserPerformanceTimeOrigin can be undefined if `performance` or
  4527. // `performance.now` doesn't exist, but this is already checked by this integration
  4528. return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000;
  4529. }
  4530. function createPaintEntry(entry) {
  4531. const { duration, entryType, name, startTime } = entry;
  4532. const start = getAbsoluteTime(startTime);
  4533. return {
  4534. type: entryType,
  4535. name,
  4536. start,
  4537. end: start + duration,
  4538. data: undefined,
  4539. };
  4540. }
  4541. function createNavigationEntry(entry) {
  4542. const {
  4543. entryType,
  4544. name,
  4545. decodedBodySize,
  4546. duration,
  4547. domComplete,
  4548. encodedBodySize,
  4549. domContentLoadedEventStart,
  4550. domContentLoadedEventEnd,
  4551. domInteractive,
  4552. loadEventStart,
  4553. loadEventEnd,
  4554. redirectCount,
  4555. startTime,
  4556. transferSize,
  4557. type,
  4558. } = entry;
  4559. // Ignore entries with no duration, they do not seem to be useful and cause dupes
  4560. if (duration === 0) {
  4561. return null;
  4562. }
  4563. return {
  4564. type: `${entryType}.${type}`,
  4565. start: getAbsoluteTime(startTime),
  4566. end: getAbsoluteTime(domComplete),
  4567. name,
  4568. data: {
  4569. size: transferSize,
  4570. decodedBodySize,
  4571. encodedBodySize,
  4572. duration,
  4573. domInteractive,
  4574. domContentLoadedEventStart,
  4575. domContentLoadedEventEnd,
  4576. loadEventStart,
  4577. loadEventEnd,
  4578. domComplete,
  4579. redirectCount,
  4580. },
  4581. };
  4582. }
  4583. function createResourceEntry(
  4584. entry,
  4585. ) {
  4586. const {
  4587. entryType,
  4588. initiatorType,
  4589. name,
  4590. responseEnd,
  4591. startTime,
  4592. decodedBodySize,
  4593. encodedBodySize,
  4594. responseStatus,
  4595. transferSize,
  4596. } = entry;
  4597. // Core SDK handles these
  4598. if (['fetch', 'xmlhttprequest'].includes(initiatorType)) {
  4599. return null;
  4600. }
  4601. return {
  4602. type: `${entryType}.${initiatorType}`,
  4603. start: getAbsoluteTime(startTime),
  4604. end: getAbsoluteTime(responseEnd),
  4605. name,
  4606. data: {
  4607. size: transferSize,
  4608. statusCode: responseStatus,
  4609. decodedBodySize,
  4610. encodedBodySize,
  4611. },
  4612. };
  4613. }
  4614. /**
  4615. * Add a LCP event to the replay based on an LCP metric.
  4616. */
  4617. function getLargestContentfulPaint(metric
  4618. ) {
  4619. const entries = metric.entries;
  4620. const lastEntry = entries[entries.length - 1] ;
  4621. const element = lastEntry ? lastEntry.element : undefined;
  4622. const value = metric.value;
  4623. const end = getAbsoluteTime(value);
  4624. const data = {
  4625. type: 'largest-contentful-paint',
  4626. name: 'largest-contentful-paint',
  4627. start: end,
  4628. end,
  4629. data: {
  4630. value,
  4631. size: value,
  4632. nodeId: element ? record.mirror.getId(element) : undefined,
  4633. },
  4634. };
  4635. return data;
  4636. }
  4637. /**
  4638. * Sets up a PerformanceObserver to listen to all performance entry types.
  4639. * Returns a callback to stop observing.
  4640. */
  4641. function setupPerformanceObserver(replay) {
  4642. function addPerformanceEntry(entry) {
  4643. // It is possible for entries to come up multiple times
  4644. if (!replay.performanceEntries.includes(entry)) {
  4645. replay.performanceEntries.push(entry);
  4646. }
  4647. }
  4648. function onEntries({ entries }) {
  4649. entries.forEach(addPerformanceEntry);
  4650. }
  4651. const clearCallbacks = [];
  4652. (['navigation', 'paint', 'resource'] ).forEach(type => {
  4653. clearCallbacks.push(addPerformanceInstrumentationHandler(type, onEntries));
  4654. });
  4655. clearCallbacks.push(
  4656. addLcpInstrumentationHandler(({ metric }) => {
  4657. replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
  4658. }),
  4659. );
  4660. // A callback to cleanup all handlers
  4661. return () => {
  4662. clearCallbacks.forEach(clearCallback => clearCallback());
  4663. };
  4664. }
  4665. /**
  4666. * 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.
  4667. *
  4668. * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
  4669. */
  4670. const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
  4671. 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});`;
  4672. function e(){const e=new Blob([r]);return URL.createObjectURL(e)}
  4673. /**
  4674. * Log a message in debug mode, and add a breadcrumb when _experiment.traceInternals is enabled.
  4675. */
  4676. function logInfo(message, shouldAddBreadcrumb) {
  4677. if (!DEBUG_BUILD) {
  4678. return;
  4679. }
  4680. logger.info(message);
  4681. if (shouldAddBreadcrumb) {
  4682. addLogBreadcrumb(message);
  4683. }
  4684. }
  4685. /**
  4686. * Log a message, and add a breadcrumb in the next tick.
  4687. * This is necessary when the breadcrumb may be added before the replay is initialized.
  4688. */
  4689. function logInfoNextTick(message, shouldAddBreadcrumb) {
  4690. if (!DEBUG_BUILD) {
  4691. return;
  4692. }
  4693. logger.info(message);
  4694. if (shouldAddBreadcrumb) {
  4695. // Wait a tick here to avoid race conditions for some initial logs
  4696. // which may be added before replay is initialized
  4697. setTimeout(() => {
  4698. addLogBreadcrumb(message);
  4699. }, 0);
  4700. }
  4701. }
  4702. function addLogBreadcrumb(message) {
  4703. addBreadcrumb(
  4704. {
  4705. category: 'console',
  4706. data: {
  4707. logger: 'replay',
  4708. },
  4709. level: 'info',
  4710. message,
  4711. },
  4712. { level: 'info' },
  4713. );
  4714. }
  4715. /** This error indicates that the event buffer size exceeded the limit.. */
  4716. class EventBufferSizeExceededError extends Error {
  4717. constructor() {
  4718. super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`);
  4719. }
  4720. }
  4721. /**
  4722. * A basic event buffer that does not do any compression.
  4723. * Used as fallback if the compression worker cannot be loaded or is disabled.
  4724. */
  4725. class EventBufferArray {
  4726. /** All the events that are buffered to be sent. */
  4727. /** @inheritdoc */
  4728. constructor() {
  4729. this.events = [];
  4730. this._totalSize = 0;
  4731. this.hasCheckout = false;
  4732. }
  4733. /** @inheritdoc */
  4734. get hasEvents() {
  4735. return this.events.length > 0;
  4736. }
  4737. /** @inheritdoc */
  4738. get type() {
  4739. return 'sync';
  4740. }
  4741. /** @inheritdoc */
  4742. destroy() {
  4743. this.events = [];
  4744. }
  4745. /** @inheritdoc */
  4746. async addEvent(event) {
  4747. const eventSize = JSON.stringify(event).length;
  4748. this._totalSize += eventSize;
  4749. if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
  4750. throw new EventBufferSizeExceededError();
  4751. }
  4752. this.events.push(event);
  4753. }
  4754. /** @inheritdoc */
  4755. finish() {
  4756. return new Promise(resolve => {
  4757. // Make a copy of the events array reference and immediately clear the
  4758. // events member so that we do not lose new events while uploading
  4759. // attachment.
  4760. const eventsRet = this.events;
  4761. this.clear();
  4762. resolve(JSON.stringify(eventsRet));
  4763. });
  4764. }
  4765. /** @inheritdoc */
  4766. clear() {
  4767. this.events = [];
  4768. this._totalSize = 0;
  4769. this.hasCheckout = false;
  4770. }
  4771. /** @inheritdoc */
  4772. getEarliestTimestamp() {
  4773. const timestamp = this.events.map(event => event.timestamp).sort()[0];
  4774. if (!timestamp) {
  4775. return null;
  4776. }
  4777. return timestampToMs(timestamp);
  4778. }
  4779. }
  4780. /**
  4781. * Event buffer that uses a web worker to compress events.
  4782. * Exported only for testing.
  4783. */
  4784. class WorkerHandler {
  4785. constructor(worker) {
  4786. this._worker = worker;
  4787. this._id = 0;
  4788. }
  4789. /**
  4790. * Ensure the worker is ready (or not).
  4791. * This will either resolve when the worker is ready, or reject if an error occured.
  4792. */
  4793. ensureReady() {
  4794. // Ensure we only check once
  4795. if (this._ensureReadyPromise) {
  4796. return this._ensureReadyPromise;
  4797. }
  4798. this._ensureReadyPromise = new Promise((resolve, reject) => {
  4799. this._worker.addEventListener(
  4800. 'message',
  4801. ({ data }) => {
  4802. if ((data ).success) {
  4803. resolve();
  4804. } else {
  4805. reject();
  4806. }
  4807. },
  4808. { once: true },
  4809. );
  4810. this._worker.addEventListener(
  4811. 'error',
  4812. error => {
  4813. reject(error);
  4814. },
  4815. { once: true },
  4816. );
  4817. });
  4818. return this._ensureReadyPromise;
  4819. }
  4820. /**
  4821. * Destroy the worker.
  4822. */
  4823. destroy() {
  4824. logInfo('[Replay] Destroying compression worker');
  4825. this._worker.terminate();
  4826. }
  4827. /**
  4828. * Post message to worker and wait for response before resolving promise.
  4829. */
  4830. postMessage(method, arg) {
  4831. const id = this._getAndIncrementId();
  4832. return new Promise((resolve, reject) => {
  4833. const listener = ({ data }) => {
  4834. const response = data ;
  4835. if (response.method !== method) {
  4836. return;
  4837. }
  4838. // There can be multiple listeners for a single method, the id ensures
  4839. // that the response matches the caller.
  4840. if (response.id !== id) {
  4841. return;
  4842. }
  4843. // At this point, we'll always want to remove listener regardless of result status
  4844. this._worker.removeEventListener('message', listener);
  4845. if (!response.success) {
  4846. // TODO: Do some error handling, not sure what
  4847. DEBUG_BUILD && logger.error('[Replay]', response.response);
  4848. reject(new Error('Error in compression worker'));
  4849. return;
  4850. }
  4851. resolve(response.response );
  4852. };
  4853. // Note: we can't use `once` option because it's possible it needs to
  4854. // listen to multiple messages
  4855. this._worker.addEventListener('message', listener);
  4856. this._worker.postMessage({ id, method, arg });
  4857. });
  4858. }
  4859. /** Get the current ID and increment it for the next call. */
  4860. _getAndIncrementId() {
  4861. return this._id++;
  4862. }
  4863. }
  4864. /**
  4865. * Event buffer that uses a web worker to compress events.
  4866. * Exported only for testing.
  4867. */
  4868. class EventBufferCompressionWorker {
  4869. /** @inheritdoc */
  4870. constructor(worker) {
  4871. this._worker = new WorkerHandler(worker);
  4872. this._earliestTimestamp = null;
  4873. this._totalSize = 0;
  4874. this.hasCheckout = false;
  4875. }
  4876. /** @inheritdoc */
  4877. get hasEvents() {
  4878. return !!this._earliestTimestamp;
  4879. }
  4880. /** @inheritdoc */
  4881. get type() {
  4882. return 'worker';
  4883. }
  4884. /**
  4885. * Ensure the worker is ready (or not).
  4886. * This will either resolve when the worker is ready, or reject if an error occured.
  4887. */
  4888. ensureReady() {
  4889. return this._worker.ensureReady();
  4890. }
  4891. /**
  4892. * Destroy the event buffer.
  4893. */
  4894. destroy() {
  4895. this._worker.destroy();
  4896. }
  4897. /**
  4898. * Add an event to the event buffer.
  4899. *
  4900. * Returns true if event was successfuly received and processed by worker.
  4901. */
  4902. addEvent(event) {
  4903. const timestamp = timestampToMs(event.timestamp);
  4904. if (!this._earliestTimestamp || timestamp < this._earliestTimestamp) {
  4905. this._earliestTimestamp = timestamp;
  4906. }
  4907. const data = JSON.stringify(event);
  4908. this._totalSize += data.length;
  4909. if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
  4910. return Promise.reject(new EventBufferSizeExceededError());
  4911. }
  4912. return this._sendEventToWorker(data);
  4913. }
  4914. /**
  4915. * Finish the event buffer and return the compressed data.
  4916. */
  4917. finish() {
  4918. return this._finishRequest();
  4919. }
  4920. /** @inheritdoc */
  4921. clear() {
  4922. this._earliestTimestamp = null;
  4923. this._totalSize = 0;
  4924. this.hasCheckout = false;
  4925. // We do not wait on this, as we assume the order of messages is consistent for the worker
  4926. this._worker.postMessage('clear').then(null, e => {
  4927. DEBUG_BUILD && logger.warn('[Replay] Sending "clear" message to worker failed', e);
  4928. });
  4929. }
  4930. /** @inheritdoc */
  4931. getEarliestTimestamp() {
  4932. return this._earliestTimestamp;
  4933. }
  4934. /**
  4935. * Send the event to the worker.
  4936. */
  4937. _sendEventToWorker(data) {
  4938. return this._worker.postMessage('addEvent', data);
  4939. }
  4940. /**
  4941. * Finish the request and return the compressed data from the worker.
  4942. */
  4943. async _finishRequest() {
  4944. const response = await this._worker.postMessage('finish');
  4945. this._earliestTimestamp = null;
  4946. this._totalSize = 0;
  4947. return response;
  4948. }
  4949. }
  4950. /**
  4951. * This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
  4952. * This can happen e.g. if the worker cannot be loaded.
  4953. * Exported only for testing.
  4954. */
  4955. class EventBufferProxy {
  4956. constructor(worker) {
  4957. this._fallback = new EventBufferArray();
  4958. this._compression = new EventBufferCompressionWorker(worker);
  4959. this._used = this._fallback;
  4960. this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded();
  4961. }
  4962. /** @inheritdoc */
  4963. get type() {
  4964. return this._used.type;
  4965. }
  4966. /** @inheritDoc */
  4967. get hasEvents() {
  4968. return this._used.hasEvents;
  4969. }
  4970. /** @inheritdoc */
  4971. get hasCheckout() {
  4972. return this._used.hasCheckout;
  4973. }
  4974. /** @inheritdoc */
  4975. set hasCheckout(value) {
  4976. this._used.hasCheckout = value;
  4977. }
  4978. /** @inheritDoc */
  4979. destroy() {
  4980. this._fallback.destroy();
  4981. this._compression.destroy();
  4982. }
  4983. /** @inheritdoc */
  4984. clear() {
  4985. return this._used.clear();
  4986. }
  4987. /** @inheritdoc */
  4988. getEarliestTimestamp() {
  4989. return this._used.getEarliestTimestamp();
  4990. }
  4991. /**
  4992. * Add an event to the event buffer.
  4993. *
  4994. * Returns true if event was successfully added.
  4995. */
  4996. addEvent(event) {
  4997. return this._used.addEvent(event);
  4998. }
  4999. /** @inheritDoc */
  5000. async finish() {
  5001. // Ensure the worker is loaded, so the sent event is compressed
  5002. await this.ensureWorkerIsLoaded();
  5003. return this._used.finish();
  5004. }
  5005. /** Ensure the worker has loaded. */
  5006. ensureWorkerIsLoaded() {
  5007. return this._ensureWorkerIsLoadedPromise;
  5008. }
  5009. /** Actually check if the worker has been loaded. */
  5010. async _ensureWorkerIsLoaded() {
  5011. try {
  5012. await this._compression.ensureReady();
  5013. } catch (error) {
  5014. // If the worker fails to load, we fall back to the simple buffer.
  5015. // Nothing more to do from our side here
  5016. logInfo('[Replay] Failed to load the compression worker, falling back to simple buffer');
  5017. return;
  5018. }
  5019. // Now we need to switch over the array buffer to the compression worker
  5020. await this._switchToCompressionWorker();
  5021. }
  5022. /** Switch the used buffer to the compression worker. */
  5023. async _switchToCompressionWorker() {
  5024. const { events, hasCheckout } = this._fallback;
  5025. const addEventPromises = [];
  5026. for (const event of events) {
  5027. addEventPromises.push(this._compression.addEvent(event));
  5028. }
  5029. this._compression.hasCheckout = hasCheckout;
  5030. // We switch over to the new buffer immediately - any further events will be added
  5031. // after the previously buffered ones
  5032. this._used = this._compression;
  5033. // Wait for original events to be re-added before resolving
  5034. try {
  5035. await Promise.all(addEventPromises);
  5036. } catch (error) {
  5037. DEBUG_BUILD && logger.warn('[Replay] Failed to add events when switching buffers.', error);
  5038. }
  5039. }
  5040. }
  5041. /**
  5042. * Create an event buffer for replays.
  5043. */
  5044. function createEventBuffer({
  5045. useCompression,
  5046. workerUrl: customWorkerUrl,
  5047. }) {
  5048. if (
  5049. useCompression &&
  5050. // eslint-disable-next-line no-restricted-globals
  5051. window.Worker
  5052. ) {
  5053. const worker = _loadWorker(customWorkerUrl);
  5054. if (worker) {
  5055. return worker;
  5056. }
  5057. }
  5058. logInfo('[Replay] Using simple buffer');
  5059. return new EventBufferArray();
  5060. }
  5061. function _loadWorker(customWorkerUrl) {
  5062. try {
  5063. const workerUrl = customWorkerUrl || _getWorkerUrl();
  5064. if (!workerUrl) {
  5065. return;
  5066. }
  5067. logInfo(`[Replay] Using compression worker${customWorkerUrl ? ` from ${customWorkerUrl}` : ''}`);
  5068. const worker = new Worker(workerUrl);
  5069. return new EventBufferProxy(worker);
  5070. } catch (error) {
  5071. logInfo('[Replay] Failed to create compression worker');
  5072. // Fall back to use simple event buffer array
  5073. }
  5074. }
  5075. function _getWorkerUrl() {
  5076. if (typeof __SENTRY_EXCLUDE_REPLAY_WORKER__ === 'undefined' || !__SENTRY_EXCLUDE_REPLAY_WORKER__) {
  5077. return e();
  5078. }
  5079. return '';
  5080. }
  5081. /** If sessionStorage is available. */
  5082. function hasSessionStorage() {
  5083. try {
  5084. // This can throw, e.g. when being accessed in a sandboxed iframe
  5085. return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage;
  5086. } catch (e) {
  5087. return false;
  5088. }
  5089. }
  5090. /**
  5091. * Removes the session from Session Storage and unsets session in replay instance
  5092. */
  5093. function clearSession(replay) {
  5094. deleteSession();
  5095. replay.session = undefined;
  5096. }
  5097. /**
  5098. * Deletes a session from storage
  5099. */
  5100. function deleteSession() {
  5101. if (!hasSessionStorage()) {
  5102. return;
  5103. }
  5104. try {
  5105. WINDOW.sessionStorage.removeItem(REPLAY_SESSION_KEY);
  5106. } catch (e) {
  5107. // Ignore potential SecurityError exceptions
  5108. }
  5109. }
  5110. /**
  5111. * Given a sample rate, returns true if replay should be sampled.
  5112. *
  5113. * 1.0 = 100% sampling
  5114. * 0.0 = 0% sampling
  5115. */
  5116. function isSampled(sampleRate) {
  5117. if (sampleRate === undefined) {
  5118. return false;
  5119. }
  5120. // Math.random() returns a number in range of 0 to 1 (inclusive of 0, but not 1)
  5121. return Math.random() < sampleRate;
  5122. }
  5123. /**
  5124. * Get a session with defaults & applied sampling.
  5125. */
  5126. function makeSession(session) {
  5127. const now = Date.now();
  5128. const id = session.id || uuid4();
  5129. // Note that this means we cannot set a started/lastActivity of `0`, but this should not be relevant outside of tests.
  5130. const started = session.started || now;
  5131. const lastActivity = session.lastActivity || now;
  5132. const segmentId = session.segmentId || 0;
  5133. const sampled = session.sampled;
  5134. const previousSessionId = session.previousSessionId;
  5135. return {
  5136. id,
  5137. started,
  5138. lastActivity,
  5139. segmentId,
  5140. sampled,
  5141. previousSessionId,
  5142. };
  5143. }
  5144. /**
  5145. * Save a session to session storage.
  5146. */
  5147. function saveSession(session) {
  5148. if (!hasSessionStorage()) {
  5149. return;
  5150. }
  5151. try {
  5152. WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session));
  5153. } catch (e) {
  5154. // Ignore potential SecurityError exceptions
  5155. }
  5156. }
  5157. /**
  5158. * Get the sampled status for a session based on sample rates & current sampled status.
  5159. */
  5160. function getSessionSampleType(sessionSampleRate, allowBuffering) {
  5161. return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false;
  5162. }
  5163. /**
  5164. * Create a new session, which in its current implementation is a Sentry event
  5165. * that all replays will be saved to as attachments. Currently, we only expect
  5166. * one of these Sentry events per "replay session".
  5167. */
  5168. function createSession(
  5169. { sessionSampleRate, allowBuffering, stickySession = false },
  5170. { previousSessionId } = {},
  5171. ) {
  5172. const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
  5173. const session = makeSession({
  5174. sampled,
  5175. previousSessionId,
  5176. });
  5177. if (stickySession) {
  5178. saveSession(session);
  5179. }
  5180. return session;
  5181. }
  5182. /**
  5183. * Fetches a session from storage
  5184. */
  5185. function fetchSession(traceInternals) {
  5186. if (!hasSessionStorage()) {
  5187. return null;
  5188. }
  5189. try {
  5190. // This can throw if cookies are disabled
  5191. const sessionStringFromStorage = WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY);
  5192. if (!sessionStringFromStorage) {
  5193. return null;
  5194. }
  5195. const sessionObj = JSON.parse(sessionStringFromStorage) ;
  5196. logInfoNextTick('[Replay] Loading existing session', traceInternals);
  5197. return makeSession(sessionObj);
  5198. } catch (e) {
  5199. return null;
  5200. }
  5201. }
  5202. /**
  5203. * Given an initial timestamp and an expiry duration, checks to see if current
  5204. * time should be considered as expired.
  5205. */
  5206. function isExpired(
  5207. initialTime,
  5208. expiry,
  5209. targetTime = +new Date(),
  5210. ) {
  5211. // Always expired if < 0
  5212. if (initialTime === null || expiry === undefined || expiry < 0) {
  5213. return true;
  5214. }
  5215. // Never expires if == 0
  5216. if (expiry === 0) {
  5217. return false;
  5218. }
  5219. return initialTime + expiry <= targetTime;
  5220. }
  5221. /**
  5222. * Checks to see if session is expired
  5223. */
  5224. function isSessionExpired(
  5225. session,
  5226. {
  5227. maxReplayDuration,
  5228. sessionIdleExpire,
  5229. targetTime = Date.now(),
  5230. },
  5231. ) {
  5232. return (
  5233. // First, check that maximum session length has not been exceeded
  5234. isExpired(session.started, maxReplayDuration, targetTime) ||
  5235. // check that the idle timeout has not been exceeded (i.e. user has
  5236. // performed an action within the last `sessionIdleExpire` ms)
  5237. isExpired(session.lastActivity, sessionIdleExpire, targetTime)
  5238. );
  5239. }
  5240. /** If the session should be refreshed or not. */
  5241. function shouldRefreshSession(
  5242. session,
  5243. { sessionIdleExpire, maxReplayDuration },
  5244. ) {
  5245. // If not expired, all good, just keep the session
  5246. if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) {
  5247. return false;
  5248. }
  5249. // If we are buffering & haven't ever flushed yet, always continue
  5250. if (session.sampled === 'buffer' && session.segmentId === 0) {
  5251. return false;
  5252. }
  5253. return true;
  5254. }
  5255. /**
  5256. * Get or create a session, when initializing the replay.
  5257. * Returns a session that may be unsampled.
  5258. */
  5259. function loadOrCreateSession(
  5260. {
  5261. traceInternals,
  5262. sessionIdleExpire,
  5263. maxReplayDuration,
  5264. previousSessionId,
  5265. }
  5266. ,
  5267. sessionOptions,
  5268. ) {
  5269. const existingSession = sessionOptions.stickySession && fetchSession(traceInternals);
  5270. // No session exists yet, just create a new one
  5271. if (!existingSession) {
  5272. logInfoNextTick('[Replay] Creating new session', traceInternals);
  5273. return createSession(sessionOptions, { previousSessionId });
  5274. }
  5275. if (!shouldRefreshSession(existingSession, { sessionIdleExpire, maxReplayDuration })) {
  5276. return existingSession;
  5277. }
  5278. logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...');
  5279. return createSession(sessionOptions, { previousSessionId: existingSession.id });
  5280. }
  5281. function isCustomEvent(event) {
  5282. return event.type === EventType.Custom;
  5283. }
  5284. /**
  5285. * Add an event to the event buffer.
  5286. * In contrast to `addEvent`, this does not return a promise & does not wait for the adding of the event to succeed/fail.
  5287. * Instead this returns `true` if we tried to add the event, else false.
  5288. * It returns `false` e.g. if we are paused, disabled, or out of the max replay duration.
  5289. *
  5290. * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
  5291. */
  5292. function addEventSync(replay, event, isCheckout) {
  5293. if (!shouldAddEvent(replay, event)) {
  5294. return false;
  5295. }
  5296. // This should never reject
  5297. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  5298. _addEvent(replay, event, isCheckout);
  5299. return true;
  5300. }
  5301. /**
  5302. * Add an event to the event buffer.
  5303. * Resolves to `null` if no event was added, else to `void`.
  5304. *
  5305. * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
  5306. */
  5307. function addEvent(
  5308. replay,
  5309. event,
  5310. isCheckout,
  5311. ) {
  5312. if (!shouldAddEvent(replay, event)) {
  5313. return Promise.resolve(null);
  5314. }
  5315. return _addEvent(replay, event, isCheckout);
  5316. }
  5317. async function _addEvent(
  5318. replay,
  5319. event,
  5320. isCheckout,
  5321. ) {
  5322. if (!replay.eventBuffer) {
  5323. return null;
  5324. }
  5325. try {
  5326. if (isCheckout && replay.recordingMode === 'buffer') {
  5327. replay.eventBuffer.clear();
  5328. }
  5329. if (isCheckout) {
  5330. replay.eventBuffer.hasCheckout = true;
  5331. }
  5332. const replayOptions = replay.getOptions();
  5333. const eventAfterPossibleCallback = maybeApplyCallback(event, replayOptions.beforeAddRecordingEvent);
  5334. if (!eventAfterPossibleCallback) {
  5335. return;
  5336. }
  5337. return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
  5338. } catch (error) {
  5339. const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
  5340. DEBUG_BUILD && logger.error(error);
  5341. await replay.stop({ reason });
  5342. const client = getClient();
  5343. if (client) {
  5344. client.recordDroppedEvent('internal_sdk_error', 'replay');
  5345. }
  5346. }
  5347. }
  5348. /** Exported only for tests. */
  5349. function shouldAddEvent(replay, event) {
  5350. if (!replay.eventBuffer || replay.isPaused() || !replay.isEnabled()) {
  5351. return false;
  5352. }
  5353. const timestampInMs = timestampToMs(event.timestamp);
  5354. // Throw out events that happen more than 5 minutes ago. This can happen if
  5355. // page has been left open and idle for a long period of time and user
  5356. // comes back to trigger a new session. The performance entries rely on
  5357. // `performance.timeOrigin`, which is when the page first opened.
  5358. if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
  5359. return false;
  5360. }
  5361. // Throw out events that are +60min from the initial timestamp
  5362. if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) {
  5363. logInfo(
  5364. `[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`,
  5365. replay.getOptions()._experiments.traceInternals,
  5366. );
  5367. return false;
  5368. }
  5369. return true;
  5370. }
  5371. function maybeApplyCallback(
  5372. event,
  5373. callback,
  5374. ) {
  5375. try {
  5376. if (typeof callback === 'function' && isCustomEvent(event)) {
  5377. return callback(event);
  5378. }
  5379. } catch (error) {
  5380. DEBUG_BUILD &&
  5381. logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error);
  5382. return null;
  5383. }
  5384. return event;
  5385. }
  5386. /** If the event is an error event */
  5387. function isErrorEvent(event) {
  5388. return !event.type;
  5389. }
  5390. /** If the event is a transaction event */
  5391. function isTransactionEvent(event) {
  5392. return event.type === 'transaction';
  5393. }
  5394. /** If the event is an replay event */
  5395. function isReplayEvent(event) {
  5396. return event.type === 'replay_event';
  5397. }
  5398. /** If the event is a feedback event */
  5399. function isFeedbackEvent(event) {
  5400. return event.type === 'feedback';
  5401. }
  5402. /**
  5403. * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`.
  5404. */
  5405. function handleAfterSendEvent(replay) {
  5406. // Custom transports may still be returning `Promise<void>`, which means we cannot expect the status code to be available there
  5407. // TODO (v8): remove this check as it will no longer be necessary
  5408. const enforceStatusCode = isBaseTransportSend();
  5409. return (event, sendResponse) => {
  5410. if (!replay.isEnabled() || (!isErrorEvent(event) && !isTransactionEvent(event))) {
  5411. return;
  5412. }
  5413. const statusCode = sendResponse && sendResponse.statusCode;
  5414. // We only want to do stuff on successful error sending, otherwise you get error replays without errors attached
  5415. // If not using the base transport, we allow `undefined` response (as a custom transport may not implement this correctly yet)
  5416. // If we do use the base transport, we skip if we encountered an non-OK status code
  5417. if (enforceStatusCode && (!statusCode || statusCode < 200 || statusCode >= 300)) {
  5418. return;
  5419. }
  5420. if (isTransactionEvent(event)) {
  5421. handleTransactionEvent(replay, event);
  5422. return;
  5423. }
  5424. handleErrorEvent(replay, event);
  5425. };
  5426. }
  5427. function handleTransactionEvent(replay, event) {
  5428. const replayContext = replay.getContext();
  5429. // Collect traceIds in _context regardless of `recordingMode`
  5430. // In error mode, _context gets cleared on every checkout
  5431. // We limit to max. 100 transactions linked
  5432. if (event.contexts && event.contexts.trace && event.contexts.trace.trace_id && replayContext.traceIds.size < 100) {
  5433. replayContext.traceIds.add(event.contexts.trace.trace_id );
  5434. }
  5435. }
  5436. function handleErrorEvent(replay, event) {
  5437. const replayContext = replay.getContext();
  5438. // Add error to list of errorIds of replay. This is ok to do even if not
  5439. // sampled because context will get reset at next checkout.
  5440. // XXX: There is also a race condition where it's possible to capture an
  5441. // error to Sentry before Replay SDK has loaded, but response returns after
  5442. // it was loaded, and this gets called.
  5443. // We limit to max. 100 errors linked
  5444. if (event.event_id && replayContext.errorIds.size < 100) {
  5445. replayContext.errorIds.add(event.event_id);
  5446. }
  5447. // If error event is tagged with replay id it means it was sampled (when in buffer mode)
  5448. // Need to be very careful that this does not cause an infinite loop
  5449. if (replay.recordingMode !== 'buffer' || !event.tags || !event.tags.replayId) {
  5450. return;
  5451. }
  5452. const { beforeErrorSampling } = replay.getOptions();
  5453. if (typeof beforeErrorSampling === 'function' && !beforeErrorSampling(event)) {
  5454. return;
  5455. }
  5456. setTimeout(() => {
  5457. // Capture current event buffer as new replay
  5458. // This should never reject
  5459. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  5460. replay.sendBufferedReplayOrFlush();
  5461. });
  5462. }
  5463. function isBaseTransportSend() {
  5464. const client = getClient();
  5465. if (!client) {
  5466. return false;
  5467. }
  5468. const transport = client.getTransport();
  5469. if (!transport) {
  5470. return false;
  5471. }
  5472. return (
  5473. (transport.send ).__sentry__baseTransport__ || false
  5474. );
  5475. }
  5476. /**
  5477. * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`.
  5478. */
  5479. function handleBeforeSendEvent(replay) {
  5480. return (event) => {
  5481. if (!replay.isEnabled() || !isErrorEvent(event)) {
  5482. return;
  5483. }
  5484. handleHydrationError(replay, event);
  5485. };
  5486. }
  5487. function handleHydrationError(replay, event) {
  5488. const exceptionValue = event.exception && event.exception.values && event.exception.values[0].value;
  5489. if (typeof exceptionValue !== 'string') {
  5490. return;
  5491. }
  5492. if (
  5493. // Only matches errors in production builds of react-dom
  5494. // Example https://reactjs.org/docs/error-decoder.html?invariant=423
  5495. exceptionValue.match(/reactjs\.org\/docs\/error-decoder\.html\?invariant=(418|419|422|423|425)/) ||
  5496. // Development builds of react-dom
  5497. // Error 1: Hydration failed because the initial UI does not match what was rendered on the server.
  5498. // Error 2: Text content does not match server-rendered HTML. Warning: Text content did not match.
  5499. exceptionValue.match(/(does not match server-rendered HTML|Hydration failed because)/i)
  5500. ) {
  5501. const breadcrumb = createBreadcrumb({
  5502. category: 'replay.hydrate-error',
  5503. });
  5504. addBreadcrumbEvent(replay, breadcrumb);
  5505. }
  5506. }
  5507. /**
  5508. * Returns true if we think the given event is an error originating inside of rrweb.
  5509. */
  5510. function isRrwebError(event, hint) {
  5511. if (event.type || !event.exception || !event.exception.values || !event.exception.values.length) {
  5512. return false;
  5513. }
  5514. // @ts-expect-error this may be set by rrweb when it finds errors
  5515. if (hint.originalException && hint.originalException.__rrweb__) {
  5516. return true;
  5517. }
  5518. return false;
  5519. }
  5520. /**
  5521. * Add a feedback breadcrumb event to replay.
  5522. */
  5523. function addFeedbackBreadcrumb(replay, event) {
  5524. replay.triggerUserActivity();
  5525. replay.addUpdate(() => {
  5526. if (!event.timestamp) {
  5527. // Ignore events that don't have timestamps (this shouldn't happen, more of a typing issue)
  5528. // Return true here so that we don't flush
  5529. return true;
  5530. }
  5531. // This should never reject
  5532. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  5533. replay.throttledAddEvent({
  5534. type: EventType.Custom,
  5535. timestamp: event.timestamp * 1000,
  5536. data: {
  5537. tag: 'breadcrumb',
  5538. payload: {
  5539. timestamp: event.timestamp,
  5540. type: 'default',
  5541. category: 'sentry.feedback',
  5542. data: {
  5543. feedbackId: event.event_id,
  5544. },
  5545. },
  5546. },
  5547. } );
  5548. return false;
  5549. });
  5550. }
  5551. /**
  5552. * Determine if event should be sampled (only applies in buffer mode).
  5553. * When an event is captured by `hanldleGlobalEvent`, when in buffer mode
  5554. * we determine if we want to sample the error or not.
  5555. */
  5556. function shouldSampleForBufferEvent(replay, event) {
  5557. if (replay.recordingMode !== 'buffer') {
  5558. return false;
  5559. }
  5560. // ignore this error because otherwise we could loop indefinitely with
  5561. // trying to capture replay and failing
  5562. if (event.message === UNABLE_TO_SEND_REPLAY) {
  5563. return false;
  5564. }
  5565. // Require the event to be an error event & to have an exception
  5566. if (!event.exception || event.type) {
  5567. return false;
  5568. }
  5569. return isSampled(replay.getOptions().errorSampleRate);
  5570. }
  5571. /**
  5572. * Returns a listener to be added to `addEventProcessor(listener)`.
  5573. */
  5574. function handleGlobalEventListener(
  5575. replay,
  5576. includeAfterSendEventHandling = false,
  5577. ) {
  5578. const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined;
  5579. return Object.assign(
  5580. (event, hint) => {
  5581. // Do nothing if replay has been disabled
  5582. if (!replay.isEnabled()) {
  5583. return event;
  5584. }
  5585. if (isReplayEvent(event)) {
  5586. // Replays have separate set of breadcrumbs, do not include breadcrumbs
  5587. // from core SDK
  5588. delete event.breadcrumbs;
  5589. return event;
  5590. }
  5591. // We only want to handle errors, transactions, and feedbacks, nothing else
  5592. if (!isErrorEvent(event) && !isTransactionEvent(event) && !isFeedbackEvent(event)) {
  5593. return event;
  5594. }
  5595. // Ensure we do not add replay_id if the session is expired
  5596. const isSessionActive = replay.checkAndHandleExpiredSession();
  5597. if (!isSessionActive) {
  5598. return event;
  5599. }
  5600. if (isFeedbackEvent(event)) {
  5601. // This should never reject
  5602. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  5603. replay.flush();
  5604. event.contexts.feedback.replay_id = replay.getSessionId();
  5605. // Add a replay breadcrumb for this piece of feedback
  5606. addFeedbackBreadcrumb(replay, event);
  5607. return event;
  5608. }
  5609. // Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb
  5610. // As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users
  5611. if (isRrwebError(event, hint) && !replay.getOptions()._experiments.captureExceptions) {
  5612. DEBUG_BUILD && logger.log('[Replay] Ignoring error from rrweb internals', event);
  5613. return null;
  5614. }
  5615. // When in buffer mode, we decide to sample here.
  5616. // Later, in `handleAfterSendEvent`, if the replayId is set, we know that we sampled
  5617. // And convert the buffer session to a full session
  5618. const isErrorEventSampled = shouldSampleForBufferEvent(replay, event);
  5619. // Tag errors if it has been sampled in buffer mode, or if it is session mode
  5620. // Only tag transactions if in session mode
  5621. const shouldTagReplayId = isErrorEventSampled || replay.recordingMode === 'session';
  5622. if (shouldTagReplayId) {
  5623. event.tags = { ...event.tags, replayId: replay.getSessionId() };
  5624. }
  5625. // In cases where a custom client is used that does not support the new hooks (yet),
  5626. // we manually call this hook method here
  5627. if (afterSendHandler) {
  5628. // Pretend the error had a 200 response so we always capture it
  5629. afterSendHandler(event, { statusCode: 200 });
  5630. }
  5631. return event;
  5632. },
  5633. { id: 'Replay' },
  5634. );
  5635. }
  5636. /**
  5637. * Create a "span" for each performance entry.
  5638. */
  5639. function createPerformanceSpans(
  5640. replay,
  5641. entries,
  5642. ) {
  5643. return entries.map(({ type, start, end, name, data }) => {
  5644. const response = replay.throttledAddEvent({
  5645. type: EventType.Custom,
  5646. timestamp: start,
  5647. data: {
  5648. tag: 'performanceSpan',
  5649. payload: {
  5650. op: type,
  5651. description: name,
  5652. startTimestamp: start,
  5653. endTimestamp: end,
  5654. data,
  5655. },
  5656. },
  5657. });
  5658. // If response is a string, it means its either THROTTLED or SKIPPED
  5659. return typeof response === 'string' ? Promise.resolve(null) : response;
  5660. });
  5661. }
  5662. function handleHistory(handlerData) {
  5663. const { from, to } = handlerData;
  5664. const now = Date.now() / 1000;
  5665. return {
  5666. type: 'navigation.push',
  5667. start: now,
  5668. end: now,
  5669. name: to,
  5670. data: {
  5671. previous: from,
  5672. },
  5673. };
  5674. }
  5675. /**
  5676. * Returns a listener to be added to `addHistoryInstrumentationHandler(listener)`.
  5677. */
  5678. function handleHistorySpanListener(replay) {
  5679. return (handlerData) => {
  5680. if (!replay.isEnabled()) {
  5681. return;
  5682. }
  5683. const result = handleHistory(handlerData);
  5684. if (result === null) {
  5685. return;
  5686. }
  5687. // Need to collect visited URLs
  5688. replay.getContext().urls.push(result.name);
  5689. replay.triggerUserActivity();
  5690. replay.addUpdate(() => {
  5691. createPerformanceSpans(replay, [result]);
  5692. // Returning false to flush
  5693. return false;
  5694. });
  5695. };
  5696. }
  5697. /**
  5698. * Check whether a given request URL should be filtered out. This is so we
  5699. * don't log Sentry ingest requests.
  5700. */
  5701. function shouldFilterRequest(replay, url) {
  5702. // If we enabled the `traceInternals` experiment, we want to trace everything
  5703. if (DEBUG_BUILD && replay.getOptions()._experiments.traceInternals) {
  5704. return false;
  5705. }
  5706. return isSentryRequestUrl(url, getClient());
  5707. }
  5708. /** Add a performance entry breadcrumb */
  5709. function addNetworkBreadcrumb(
  5710. replay,
  5711. result,
  5712. ) {
  5713. if (!replay.isEnabled()) {
  5714. return;
  5715. }
  5716. if (result === null) {
  5717. return;
  5718. }
  5719. if (shouldFilterRequest(replay, result.name)) {
  5720. return;
  5721. }
  5722. replay.addUpdate(() => {
  5723. createPerformanceSpans(replay, [result]);
  5724. // Returning true will cause `addUpdate` to not flush
  5725. // We do not want network requests to cause a flush. This will prevent
  5726. // recurring/polling requests from keeping the replay session alive.
  5727. return true;
  5728. });
  5729. }
  5730. /** only exported for tests */
  5731. function handleFetch(handlerData) {
  5732. const { startTimestamp, endTimestamp, fetchData, response } = handlerData;
  5733. if (!endTimestamp) {
  5734. return null;
  5735. }
  5736. // This is only used as a fallback, so we know the body sizes are never set here
  5737. const { method, url } = fetchData;
  5738. return {
  5739. type: 'resource.fetch',
  5740. start: startTimestamp / 1000,
  5741. end: endTimestamp / 1000,
  5742. name: url,
  5743. data: {
  5744. method,
  5745. statusCode: response ? (response ).status : undefined,
  5746. },
  5747. };
  5748. }
  5749. /**
  5750. * Returns a listener to be added to `addFetchInstrumentationHandler(listener)`.
  5751. */
  5752. function handleFetchSpanListener(replay) {
  5753. return (handlerData) => {
  5754. if (!replay.isEnabled()) {
  5755. return;
  5756. }
  5757. const result = handleFetch(handlerData);
  5758. addNetworkBreadcrumb(replay, result);
  5759. };
  5760. }
  5761. /** only exported for tests */
  5762. function handleXhr(handlerData) {
  5763. const { startTimestamp, endTimestamp, xhr } = handlerData;
  5764. const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY];
  5765. if (!startTimestamp || !endTimestamp || !sentryXhrData) {
  5766. return null;
  5767. }
  5768. // This is only used as a fallback, so we know the body sizes are never set here
  5769. const { method, url, status_code: statusCode } = sentryXhrData;
  5770. if (url === undefined) {
  5771. return null;
  5772. }
  5773. return {
  5774. type: 'resource.xhr',
  5775. name: url,
  5776. start: startTimestamp / 1000,
  5777. end: endTimestamp / 1000,
  5778. data: {
  5779. method,
  5780. statusCode,
  5781. },
  5782. };
  5783. }
  5784. /**
  5785. * Returns a listener to be added to `addXhrInstrumentationHandler(listener)`.
  5786. */
  5787. function handleXhrSpanListener(replay) {
  5788. return (handlerData) => {
  5789. if (!replay.isEnabled()) {
  5790. return;
  5791. }
  5792. const result = handleXhr(handlerData);
  5793. addNetworkBreadcrumb(replay, result);
  5794. };
  5795. }
  5796. /** Get the size of a body. */
  5797. function getBodySize(
  5798. body,
  5799. textEncoder,
  5800. ) {
  5801. if (!body) {
  5802. return undefined;
  5803. }
  5804. try {
  5805. if (typeof body === 'string') {
  5806. return textEncoder.encode(body).length;
  5807. }
  5808. if (body instanceof URLSearchParams) {
  5809. return textEncoder.encode(body.toString()).length;
  5810. }
  5811. if (body instanceof FormData) {
  5812. const formDataStr = _serializeFormData(body);
  5813. return textEncoder.encode(formDataStr).length;
  5814. }
  5815. if (body instanceof Blob) {
  5816. return body.size;
  5817. }
  5818. if (body instanceof ArrayBuffer) {
  5819. return body.byteLength;
  5820. }
  5821. // Currently unhandled types: ArrayBufferView, ReadableStream
  5822. } catch (e) {
  5823. // just return undefined
  5824. }
  5825. return undefined;
  5826. }
  5827. /** Convert a Content-Length header to number/undefined. */
  5828. function parseContentLengthHeader(header) {
  5829. if (!header) {
  5830. return undefined;
  5831. }
  5832. const size = parseInt(header, 10);
  5833. return isNaN(size) ? undefined : size;
  5834. }
  5835. /** Get the string representation of a body. */
  5836. function getBodyString(body) {
  5837. try {
  5838. if (typeof body === 'string') {
  5839. return [body];
  5840. }
  5841. if (body instanceof URLSearchParams) {
  5842. return [body.toString()];
  5843. }
  5844. if (body instanceof FormData) {
  5845. return [_serializeFormData(body)];
  5846. }
  5847. if (!body) {
  5848. return [undefined];
  5849. }
  5850. } catch (e2) {
  5851. DEBUG_BUILD && logger.warn('[Replay] Failed to serialize body', body);
  5852. return [undefined, 'BODY_PARSE_ERROR'];
  5853. }
  5854. DEBUG_BUILD && logger.info('[Replay] Skipping network body because of body type', body);
  5855. return [undefined, 'UNPARSEABLE_BODY_TYPE'];
  5856. }
  5857. /** Merge a warning into an existing network request/response. */
  5858. function mergeWarning(
  5859. info,
  5860. warning,
  5861. ) {
  5862. if (!info) {
  5863. return {
  5864. headers: {},
  5865. size: undefined,
  5866. _meta: {
  5867. warnings: [warning],
  5868. },
  5869. };
  5870. }
  5871. const newMeta = { ...info._meta };
  5872. const existingWarnings = newMeta.warnings || [];
  5873. newMeta.warnings = [...existingWarnings, warning];
  5874. info._meta = newMeta;
  5875. return info;
  5876. }
  5877. /** Convert ReplayNetworkRequestData to a PerformanceEntry. */
  5878. function makeNetworkReplayBreadcrumb(
  5879. type,
  5880. data,
  5881. ) {
  5882. if (!data) {
  5883. return null;
  5884. }
  5885. const { startTimestamp, endTimestamp, url, method, statusCode, request, response } = data;
  5886. const result = {
  5887. type,
  5888. start: startTimestamp / 1000,
  5889. end: endTimestamp / 1000,
  5890. name: url,
  5891. data: dropUndefinedKeys({
  5892. method,
  5893. statusCode,
  5894. request,
  5895. response,
  5896. }),
  5897. };
  5898. return result;
  5899. }
  5900. /** Build the request or response part of a replay network breadcrumb that was skipped. */
  5901. function buildSkippedNetworkRequestOrResponse(bodySize) {
  5902. return {
  5903. headers: {},
  5904. size: bodySize,
  5905. _meta: {
  5906. warnings: ['URL_SKIPPED'],
  5907. },
  5908. };
  5909. }
  5910. /** Build the request or response part of a replay network breadcrumb. */
  5911. function buildNetworkRequestOrResponse(
  5912. headers,
  5913. bodySize,
  5914. body,
  5915. ) {
  5916. if (!bodySize && Object.keys(headers).length === 0) {
  5917. return undefined;
  5918. }
  5919. if (!bodySize) {
  5920. return {
  5921. headers,
  5922. };
  5923. }
  5924. if (!body) {
  5925. return {
  5926. headers,
  5927. size: bodySize,
  5928. };
  5929. }
  5930. const info = {
  5931. headers,
  5932. size: bodySize,
  5933. };
  5934. const { body: normalizedBody, warnings } = normalizeNetworkBody(body);
  5935. info.body = normalizedBody;
  5936. if (warnings && warnings.length > 0) {
  5937. info._meta = {
  5938. warnings,
  5939. };
  5940. }
  5941. return info;
  5942. }
  5943. /** Filter a set of headers */
  5944. function getAllowedHeaders(headers, allowedHeaders) {
  5945. return Object.keys(headers).reduce((filteredHeaders, key) => {
  5946. const normalizedKey = key.toLowerCase();
  5947. // Avoid putting empty strings into the headers
  5948. if (allowedHeaders.includes(normalizedKey) && headers[key]) {
  5949. filteredHeaders[normalizedKey] = headers[key];
  5950. }
  5951. return filteredHeaders;
  5952. }, {});
  5953. }
  5954. function _serializeFormData(formData) {
  5955. // This is a bit simplified, but gives us a decent estimate
  5956. // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13'
  5957. // @ts-expect-error passing FormData to URLSearchParams actually works
  5958. return new URLSearchParams(formData).toString();
  5959. }
  5960. function normalizeNetworkBody(body)
  5961. {
  5962. if (!body || typeof body !== 'string') {
  5963. return {
  5964. body,
  5965. };
  5966. }
  5967. const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE;
  5968. const isProbablyJson = _strIsProbablyJson(body);
  5969. if (exceedsSizeLimit) {
  5970. const truncatedBody = body.slice(0, NETWORK_BODY_MAX_SIZE);
  5971. if (isProbablyJson) {
  5972. return {
  5973. body: truncatedBody,
  5974. warnings: ['MAYBE_JSON_TRUNCATED'],
  5975. };
  5976. }
  5977. return {
  5978. body: `${truncatedBody}…`,
  5979. warnings: ['TEXT_TRUNCATED'],
  5980. };
  5981. }
  5982. if (isProbablyJson) {
  5983. try {
  5984. const jsonBody = JSON.parse(body);
  5985. return {
  5986. body: jsonBody,
  5987. };
  5988. } catch (e3) {
  5989. // fall back to just send the body as string
  5990. }
  5991. }
  5992. return {
  5993. body,
  5994. };
  5995. }
  5996. function _strIsProbablyJson(str) {
  5997. const first = str[0];
  5998. const last = str[str.length - 1];
  5999. // Simple check: If this does not start & end with {} or [], it's not JSON
  6000. return (first === '[' && last === ']') || (first === '{' && last === '}');
  6001. }
  6002. /** Match an URL against a list of strings/Regex. */
  6003. function urlMatches(url, urls) {
  6004. const fullUrl = getFullUrl(url);
  6005. return stringMatchesSomePattern(fullUrl, urls);
  6006. }
  6007. /** exported for tests */
  6008. function getFullUrl(url, baseURI = WINDOW.document.baseURI) {
  6009. // Short circuit for common cases:
  6010. if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith(WINDOW.location.origin)) {
  6011. return url;
  6012. }
  6013. const fixedUrl = new URL(url, baseURI);
  6014. // If these do not match, we are not dealing with a relative URL, so just return it
  6015. if (fixedUrl.origin !== new URL(baseURI).origin) {
  6016. return url;
  6017. }
  6018. const fullUrl = fixedUrl.href;
  6019. // Remove trailing slashes, if they don't match the original URL
  6020. if (!url.endsWith('/') && fullUrl.endsWith('/')) {
  6021. return fullUrl.slice(0, -1);
  6022. }
  6023. return fullUrl;
  6024. }
  6025. /**
  6026. * Capture a fetch breadcrumb to a replay.
  6027. * This adds additional data (where approriate).
  6028. */
  6029. async function captureFetchBreadcrumbToReplay(
  6030. breadcrumb,
  6031. hint,
  6032. options
  6033. ,
  6034. ) {
  6035. try {
  6036. const data = await _prepareFetchData(breadcrumb, hint, options);
  6037. // Create a replay performance entry from this breadcrumb
  6038. const result = makeNetworkReplayBreadcrumb('resource.fetch', data);
  6039. addNetworkBreadcrumb(options.replay, result);
  6040. } catch (error) {
  6041. DEBUG_BUILD && logger.error('[Replay] Failed to capture fetch breadcrumb', error);
  6042. }
  6043. }
  6044. /**
  6045. * Enrich a breadcrumb with additional data.
  6046. * This has to be sync & mutate the given breadcrumb,
  6047. * as the breadcrumb is afterwards consumed by other handlers.
  6048. */
  6049. function enrichFetchBreadcrumb(
  6050. breadcrumb,
  6051. hint,
  6052. options,
  6053. ) {
  6054. const { input, response } = hint;
  6055. const body = input ? _getFetchRequestArgBody(input) : undefined;
  6056. const reqSize = getBodySize(body, options.textEncoder);
  6057. const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined;
  6058. if (reqSize !== undefined) {
  6059. breadcrumb.data.request_body_size = reqSize;
  6060. }
  6061. if (resSize !== undefined) {
  6062. breadcrumb.data.response_body_size = resSize;
  6063. }
  6064. }
  6065. async function _prepareFetchData(
  6066. breadcrumb,
  6067. hint,
  6068. options
  6069. ,
  6070. ) {
  6071. const now = Date.now();
  6072. const { startTimestamp = now, endTimestamp = now } = hint;
  6073. const {
  6074. url,
  6075. method,
  6076. status_code: statusCode = 0,
  6077. request_body_size: requestBodySize,
  6078. response_body_size: responseBodySize,
  6079. } = breadcrumb.data;
  6080. const captureDetails =
  6081. urlMatches(url, options.networkDetailAllowUrls) && !urlMatches(url, options.networkDetailDenyUrls);
  6082. const request = captureDetails
  6083. ? _getRequestInfo(options, hint.input, requestBodySize)
  6084. : buildSkippedNetworkRequestOrResponse(requestBodySize);
  6085. const response = await _getResponseInfo(captureDetails, options, hint.response, responseBodySize);
  6086. return {
  6087. startTimestamp,
  6088. endTimestamp,
  6089. url,
  6090. method,
  6091. statusCode,
  6092. request,
  6093. response,
  6094. };
  6095. }
  6096. function _getRequestInfo(
  6097. { networkCaptureBodies, networkRequestHeaders },
  6098. input,
  6099. requestBodySize,
  6100. ) {
  6101. const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {};
  6102. if (!networkCaptureBodies) {
  6103. return buildNetworkRequestOrResponse(headers, requestBodySize, undefined);
  6104. }
  6105. // We only want to transmit string or string-like bodies
  6106. const requestBody = _getFetchRequestArgBody(input);
  6107. const [bodyStr, warning] = getBodyString(requestBody);
  6108. const data = buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr);
  6109. if (warning) {
  6110. return mergeWarning(data, warning);
  6111. }
  6112. return data;
  6113. }
  6114. /** Exported only for tests. */
  6115. async function _getResponseInfo(
  6116. captureDetails,
  6117. {
  6118. networkCaptureBodies,
  6119. textEncoder,
  6120. networkResponseHeaders,
  6121. }
  6122. ,
  6123. response,
  6124. responseBodySize,
  6125. ) {
  6126. if (!captureDetails && responseBodySize !== undefined) {
  6127. return buildSkippedNetworkRequestOrResponse(responseBodySize);
  6128. }
  6129. const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {};
  6130. if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) {
  6131. return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
  6132. }
  6133. const [bodyText, warning] = await _parseFetchResponseBody(response);
  6134. const result = getResponseData(bodyText, {
  6135. networkCaptureBodies,
  6136. textEncoder,
  6137. responseBodySize,
  6138. captureDetails,
  6139. headers,
  6140. });
  6141. if (warning) {
  6142. return mergeWarning(result, warning);
  6143. }
  6144. return result;
  6145. }
  6146. function getResponseData(
  6147. bodyText,
  6148. {
  6149. networkCaptureBodies,
  6150. textEncoder,
  6151. responseBodySize,
  6152. captureDetails,
  6153. headers,
  6154. }
  6155. ,
  6156. ) {
  6157. try {
  6158. const size =
  6159. bodyText && bodyText.length && responseBodySize === undefined
  6160. ? getBodySize(bodyText, textEncoder)
  6161. : responseBodySize;
  6162. if (!captureDetails) {
  6163. return buildSkippedNetworkRequestOrResponse(size);
  6164. }
  6165. if (networkCaptureBodies) {
  6166. return buildNetworkRequestOrResponse(headers, size, bodyText);
  6167. }
  6168. return buildNetworkRequestOrResponse(headers, size, undefined);
  6169. } catch (error) {
  6170. DEBUG_BUILD && logger.warn('[Replay] Failed to serialize response body', error);
  6171. // fallback
  6172. return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
  6173. }
  6174. }
  6175. async function _parseFetchResponseBody(response) {
  6176. const res = _tryCloneResponse(response);
  6177. if (!res) {
  6178. return [undefined, 'BODY_PARSE_ERROR'];
  6179. }
  6180. try {
  6181. const text = await _tryGetResponseText(res);
  6182. return [text];
  6183. } catch (error) {
  6184. DEBUG_BUILD && logger.warn('[Replay] Failed to get text body from response', error);
  6185. return [undefined, 'BODY_PARSE_ERROR'];
  6186. }
  6187. }
  6188. function _getFetchRequestArgBody(fetchArgs = []) {
  6189. // We only support getting the body from the fetch options
  6190. if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') {
  6191. return undefined;
  6192. }
  6193. return (fetchArgs[1] ).body;
  6194. }
  6195. function getAllHeaders(headers, allowedHeaders) {
  6196. const allHeaders = {};
  6197. allowedHeaders.forEach(header => {
  6198. if (headers.get(header)) {
  6199. allHeaders[header] = headers.get(header) ;
  6200. }
  6201. });
  6202. return allHeaders;
  6203. }
  6204. function getRequestHeaders(fetchArgs, allowedHeaders) {
  6205. if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') {
  6206. return getHeadersFromOptions(fetchArgs[0] , allowedHeaders);
  6207. }
  6208. if (fetchArgs.length === 2) {
  6209. return getHeadersFromOptions(fetchArgs[1] , allowedHeaders);
  6210. }
  6211. return {};
  6212. }
  6213. function getHeadersFromOptions(
  6214. input,
  6215. allowedHeaders,
  6216. ) {
  6217. if (!input) {
  6218. return {};
  6219. }
  6220. const headers = input.headers;
  6221. if (!headers) {
  6222. return {};
  6223. }
  6224. if (headers instanceof Headers) {
  6225. return getAllHeaders(headers, allowedHeaders);
  6226. }
  6227. // We do not support this, as it is not really documented (anymore?)
  6228. if (Array.isArray(headers)) {
  6229. return {};
  6230. }
  6231. return getAllowedHeaders(headers, allowedHeaders);
  6232. }
  6233. function _tryCloneResponse(response) {
  6234. try {
  6235. // We have to clone this, as the body can only be read once
  6236. return response.clone();
  6237. } catch (error) {
  6238. // this can throw if the response was already consumed before
  6239. DEBUG_BUILD && logger.warn('[Replay] Failed to clone response body', error);
  6240. }
  6241. }
  6242. /**
  6243. * Get the response body of a fetch request, or timeout after 500ms.
  6244. * Fetch can return a streaming body, that may not resolve (or not for a long time).
  6245. * If that happens, we rather abort after a short time than keep waiting for this.
  6246. */
  6247. function _tryGetResponseText(response) {
  6248. return new Promise((resolve, reject) => {
  6249. const timeout = setTimeout(() => reject(new Error('Timeout while trying to read response body')), 500);
  6250. _getResponseText(response)
  6251. .then(
  6252. txt => resolve(txt),
  6253. reason => reject(reason),
  6254. )
  6255. .finally(() => clearTimeout(timeout));
  6256. });
  6257. }
  6258. async function _getResponseText(response) {
  6259. // Force this to be a promise, just to be safe
  6260. // eslint-disable-next-line no-return-await
  6261. return await response.text();
  6262. }
  6263. /**
  6264. * Capture an XHR breadcrumb to a replay.
  6265. * This adds additional data (where approriate).
  6266. */
  6267. async function captureXhrBreadcrumbToReplay(
  6268. breadcrumb,
  6269. hint,
  6270. options,
  6271. ) {
  6272. try {
  6273. const data = _prepareXhrData(breadcrumb, hint, options);
  6274. // Create a replay performance entry from this breadcrumb
  6275. const result = makeNetworkReplayBreadcrumb('resource.xhr', data);
  6276. addNetworkBreadcrumb(options.replay, result);
  6277. } catch (error) {
  6278. DEBUG_BUILD && logger.error('[Replay] Failed to capture xhr breadcrumb', error);
  6279. }
  6280. }
  6281. /**
  6282. * Enrich a breadcrumb with additional data.
  6283. * This has to be sync & mutate the given breadcrumb,
  6284. * as the breadcrumb is afterwards consumed by other handlers.
  6285. */
  6286. function enrichXhrBreadcrumb(
  6287. breadcrumb,
  6288. hint,
  6289. options,
  6290. ) {
  6291. const { xhr, input } = hint;
  6292. if (!xhr) {
  6293. return;
  6294. }
  6295. const reqSize = getBodySize(input, options.textEncoder);
  6296. const resSize = xhr.getResponseHeader('content-length')
  6297. ? parseContentLengthHeader(xhr.getResponseHeader('content-length'))
  6298. : _getBodySize(xhr.response, xhr.responseType, options.textEncoder);
  6299. if (reqSize !== undefined) {
  6300. breadcrumb.data.request_body_size = reqSize;
  6301. }
  6302. if (resSize !== undefined) {
  6303. breadcrumb.data.response_body_size = resSize;
  6304. }
  6305. }
  6306. function _prepareXhrData(
  6307. breadcrumb,
  6308. hint,
  6309. options,
  6310. ) {
  6311. const now = Date.now();
  6312. const { startTimestamp = now, endTimestamp = now, input, xhr } = hint;
  6313. const {
  6314. url,
  6315. method,
  6316. status_code: statusCode = 0,
  6317. request_body_size: requestBodySize,
  6318. response_body_size: responseBodySize,
  6319. } = breadcrumb.data;
  6320. if (!url) {
  6321. return null;
  6322. }
  6323. if (!xhr || !urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
  6324. const request = buildSkippedNetworkRequestOrResponse(requestBodySize);
  6325. const response = buildSkippedNetworkRequestOrResponse(responseBodySize);
  6326. return {
  6327. startTimestamp,
  6328. endTimestamp,
  6329. url,
  6330. method,
  6331. statusCode,
  6332. request,
  6333. response,
  6334. };
  6335. }
  6336. const xhrInfo = xhr[SENTRY_XHR_DATA_KEY];
  6337. const networkRequestHeaders = xhrInfo
  6338. ? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders)
  6339. : {};
  6340. const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders);
  6341. const [requestBody, requestWarning] = options.networkCaptureBodies ? getBodyString(input) : [undefined];
  6342. const [responseBody, responseWarning] = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : [undefined];
  6343. const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody);
  6344. const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody);
  6345. return {
  6346. startTimestamp,
  6347. endTimestamp,
  6348. url,
  6349. method,
  6350. statusCode,
  6351. request: requestWarning ? mergeWarning(request, requestWarning) : request,
  6352. response: responseWarning ? mergeWarning(response, responseWarning) : response,
  6353. };
  6354. }
  6355. function getResponseHeaders(xhr) {
  6356. const headers = xhr.getAllResponseHeaders();
  6357. if (!headers) {
  6358. return {};
  6359. }
  6360. return headers.split('\r\n').reduce((acc, line) => {
  6361. const [key, value] = line.split(': ');
  6362. acc[key.toLowerCase()] = value;
  6363. return acc;
  6364. }, {});
  6365. }
  6366. function _getXhrResponseBody(xhr) {
  6367. // We collect errors that happen, but only log them if we can't get any response body
  6368. const errors = [];
  6369. try {
  6370. return [xhr.responseText];
  6371. } catch (e) {
  6372. errors.push(e);
  6373. }
  6374. // Try to manually parse the response body, if responseText fails
  6375. try {
  6376. return _parseXhrResponse(xhr.response, xhr.responseType);
  6377. } catch (e) {
  6378. errors.push(e);
  6379. }
  6380. DEBUG_BUILD && logger.warn('[Replay] Failed to get xhr response body', ...errors);
  6381. return [undefined];
  6382. }
  6383. /**
  6384. * Get the string representation of the XHR response.
  6385. * Based on MDN, these are the possible types of the response:
  6386. * string
  6387. * ArrayBuffer
  6388. * Blob
  6389. * Document
  6390. * POJO
  6391. *
  6392. * Exported only for tests.
  6393. */
  6394. function _parseXhrResponse(
  6395. body,
  6396. responseType,
  6397. ) {
  6398. try {
  6399. if (typeof body === 'string') {
  6400. return [body];
  6401. }
  6402. if (body instanceof Document) {
  6403. return [body.body.outerHTML];
  6404. }
  6405. if (responseType === 'json' && body && typeof body === 'object') {
  6406. return [JSON.stringify(body)];
  6407. }
  6408. if (!body) {
  6409. return [undefined];
  6410. }
  6411. } catch (e2) {
  6412. DEBUG_BUILD && logger.warn('[Replay] Failed to serialize body', body);
  6413. return [undefined, 'BODY_PARSE_ERROR'];
  6414. }
  6415. DEBUG_BUILD && logger.info('[Replay] Skipping network body because of body type', body);
  6416. return [undefined, 'UNPARSEABLE_BODY_TYPE'];
  6417. }
  6418. function _getBodySize(
  6419. body,
  6420. responseType,
  6421. textEncoder,
  6422. ) {
  6423. try {
  6424. const bodyStr = responseType === 'json' && body && typeof body === 'object' ? JSON.stringify(body) : body;
  6425. return getBodySize(bodyStr, textEncoder);
  6426. } catch (e3) {
  6427. return undefined;
  6428. }
  6429. }
  6430. /**
  6431. * This method does two things:
  6432. * - It enriches the regular XHR/fetch breadcrumbs with request/response size data
  6433. * - It captures the XHR/fetch breadcrumbs to the replay
  6434. * (enriching it with further data that is _not_ added to the regular breadcrumbs)
  6435. */
  6436. function handleNetworkBreadcrumbs(replay) {
  6437. const client = getClient();
  6438. try {
  6439. const textEncoder = new TextEncoder();
  6440. const {
  6441. networkDetailAllowUrls,
  6442. networkDetailDenyUrls,
  6443. networkCaptureBodies,
  6444. networkRequestHeaders,
  6445. networkResponseHeaders,
  6446. } = replay.getOptions();
  6447. const options = {
  6448. replay,
  6449. textEncoder,
  6450. networkDetailAllowUrls,
  6451. networkDetailDenyUrls,
  6452. networkCaptureBodies,
  6453. networkRequestHeaders,
  6454. networkResponseHeaders,
  6455. };
  6456. if (client && client.on) {
  6457. client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint));
  6458. } else {
  6459. // Fallback behavior
  6460. addFetchInstrumentationHandler(handleFetchSpanListener(replay));
  6461. addXhrInstrumentationHandler(handleXhrSpanListener(replay));
  6462. }
  6463. } catch (e2) {
  6464. // Do nothing
  6465. }
  6466. }
  6467. /** just exported for tests */
  6468. function beforeAddNetworkBreadcrumb(
  6469. options,
  6470. breadcrumb,
  6471. hint,
  6472. ) {
  6473. if (!breadcrumb.data) {
  6474. return;
  6475. }
  6476. try {
  6477. if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) {
  6478. // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
  6479. // Because the hook runs synchronously, and the breadcrumb is afterwards passed on
  6480. // So any async mutations to it will not be reflected in the final breadcrumb
  6481. enrichXhrBreadcrumb(breadcrumb, hint, options);
  6482. // This call should not reject
  6483. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  6484. captureXhrBreadcrumbToReplay(breadcrumb, hint, options);
  6485. }
  6486. if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) {
  6487. // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
  6488. // Because the hook runs synchronously, and the breadcrumb is afterwards passed on
  6489. // So any async mutations to it will not be reflected in the final breadcrumb
  6490. enrichFetchBreadcrumb(breadcrumb, hint, options);
  6491. // This call should not reject
  6492. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  6493. captureFetchBreadcrumbToReplay(breadcrumb, hint, options);
  6494. }
  6495. } catch (e) {
  6496. DEBUG_BUILD && logger.warn('Error when enriching network breadcrumb');
  6497. }
  6498. }
  6499. function _isXhrBreadcrumb(breadcrumb) {
  6500. return breadcrumb.category === 'xhr';
  6501. }
  6502. function _isFetchBreadcrumb(breadcrumb) {
  6503. return breadcrumb.category === 'fetch';
  6504. }
  6505. function _isXhrHint(hint) {
  6506. return hint && hint.xhr;
  6507. }
  6508. function _isFetchHint(hint) {
  6509. return hint && hint.response;
  6510. }
  6511. let _LAST_BREADCRUMB = null;
  6512. function isBreadcrumbWithCategory(breadcrumb) {
  6513. return !!breadcrumb.category;
  6514. }
  6515. const handleScopeListener =
  6516. (replay) =>
  6517. (scope) => {
  6518. if (!replay.isEnabled()) {
  6519. return;
  6520. }
  6521. const result = handleScope(scope);
  6522. if (!result) {
  6523. return;
  6524. }
  6525. addBreadcrumbEvent(replay, result);
  6526. };
  6527. /**
  6528. * An event handler to handle scope changes.
  6529. */
  6530. function handleScope(scope) {
  6531. // TODO (v8): Remove this guard. This was put in place because we introduced
  6532. // Scope.getLastBreadcrumb mid-v7 which caused incompatibilities with older SDKs.
  6533. // For now, we'll just return null if the method doesn't exist but we should eventually
  6534. // get rid of this guard.
  6535. const newBreadcrumb = scope.getLastBreadcrumb && scope.getLastBreadcrumb();
  6536. // Listener can be called when breadcrumbs have not changed, so we store the
  6537. // reference to the last crumb and only return a crumb if it has changed
  6538. if (_LAST_BREADCRUMB === newBreadcrumb || !newBreadcrumb) {
  6539. return null;
  6540. }
  6541. _LAST_BREADCRUMB = newBreadcrumb;
  6542. if (
  6543. !isBreadcrumbWithCategory(newBreadcrumb) ||
  6544. ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) ||
  6545. newBreadcrumb.category.startsWith('ui.')
  6546. ) {
  6547. return null;
  6548. }
  6549. if (newBreadcrumb.category === 'console') {
  6550. return normalizeConsoleBreadcrumb(newBreadcrumb);
  6551. }
  6552. return createBreadcrumb(newBreadcrumb);
  6553. }
  6554. /** exported for tests only */
  6555. function normalizeConsoleBreadcrumb(
  6556. breadcrumb,
  6557. ) {
  6558. const args = breadcrumb.data && breadcrumb.data.arguments;
  6559. if (!Array.isArray(args) || args.length === 0) {
  6560. return createBreadcrumb(breadcrumb);
  6561. }
  6562. let isTruncated = false;
  6563. // Avoid giant args captures
  6564. const normalizedArgs = args.map(arg => {
  6565. if (!arg) {
  6566. return arg;
  6567. }
  6568. if (typeof arg === 'string') {
  6569. if (arg.length > CONSOLE_ARG_MAX_SIZE) {
  6570. isTruncated = true;
  6571. return `${arg.slice(0, CONSOLE_ARG_MAX_SIZE)}…`;
  6572. }
  6573. return arg;
  6574. }
  6575. if (typeof arg === 'object') {
  6576. try {
  6577. const normalizedArg = normalize(arg, 7);
  6578. const stringified = JSON.stringify(normalizedArg);
  6579. if (stringified.length > CONSOLE_ARG_MAX_SIZE) {
  6580. isTruncated = true;
  6581. // We use the pretty printed JSON string here as a base
  6582. return `${JSON.stringify(normalizedArg, null, 2).slice(0, CONSOLE_ARG_MAX_SIZE)}…`;
  6583. }
  6584. return normalizedArg;
  6585. } catch (e) {
  6586. // fall back to default
  6587. }
  6588. }
  6589. return arg;
  6590. });
  6591. return createBreadcrumb({
  6592. ...breadcrumb,
  6593. data: {
  6594. ...breadcrumb.data,
  6595. arguments: normalizedArgs,
  6596. ...(isTruncated ? { _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] } } : {}),
  6597. },
  6598. });
  6599. }
  6600. /**
  6601. * Add global listeners that cannot be removed.
  6602. */
  6603. function addGlobalListeners(replay) {
  6604. // Listeners from core SDK //
  6605. const scope = getCurrentScope();
  6606. const client = getClient();
  6607. scope.addScopeListener(handleScopeListener(replay));
  6608. addClickKeypressInstrumentationHandler(handleDomListener(replay));
  6609. addHistoryInstrumentationHandler(handleHistorySpanListener(replay));
  6610. handleNetworkBreadcrumbs(replay);
  6611. // Tag all (non replay) events that get sent to Sentry with the current
  6612. // replay ID so that we can reference them later in the UI
  6613. const eventProcessor = handleGlobalEventListener(replay, !hasHooks(client));
  6614. if (client && client.addEventProcessor) {
  6615. client.addEventProcessor(eventProcessor);
  6616. } else {
  6617. addEventProcessor(eventProcessor);
  6618. }
  6619. // If a custom client has no hooks yet, we continue to use the "old" implementation
  6620. if (hasHooks(client)) {
  6621. client.on('beforeSendEvent', handleBeforeSendEvent(replay));
  6622. client.on('afterSendEvent', handleAfterSendEvent(replay));
  6623. client.on('createDsc', (dsc) => {
  6624. const replayId = replay.getSessionId();
  6625. // We do not want to set the DSC when in buffer mode, as that means the replay has not been sent (yet)
  6626. if (replayId && replay.isEnabled() && replay.recordingMode === 'session') {
  6627. // Ensure to check that the session is still active - it could have expired in the meanwhile
  6628. const isSessionActive = replay.checkAndHandleExpiredSession();
  6629. if (isSessionActive) {
  6630. dsc.replay_id = replayId;
  6631. }
  6632. }
  6633. });
  6634. client.on('startTransaction', transaction => {
  6635. replay.lastTransaction = transaction;
  6636. });
  6637. // We may be missing the initial startTransaction due to timing issues,
  6638. // so we capture it on finish again.
  6639. client.on('finishTransaction', transaction => {
  6640. replay.lastTransaction = transaction;
  6641. });
  6642. // We want to flush replay
  6643. client.on('beforeSendFeedback', (feedbackEvent, options) => {
  6644. const replayId = replay.getSessionId();
  6645. if (options && options.includeReplay && replay.isEnabled() && replayId) {
  6646. // This should never reject
  6647. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  6648. replay.flush();
  6649. if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) {
  6650. feedbackEvent.contexts.feedback.replay_id = replayId;
  6651. }
  6652. }
  6653. });
  6654. }
  6655. }
  6656. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  6657. function hasHooks(client) {
  6658. return !!(client && client.on);
  6659. }
  6660. /**
  6661. * Create a "span" for the total amount of memory being used by JS objects
  6662. * (including v8 internal objects).
  6663. */
  6664. async function addMemoryEntry(replay) {
  6665. // window.performance.memory is a non-standard API and doesn't work on all browsers, so we try-catch this
  6666. try {
  6667. return Promise.all(
  6668. createPerformanceSpans(replay, [
  6669. // @ts-expect-error memory doesn't exist on type Performance as the API is non-standard (we check that it exists above)
  6670. createMemoryEntry(WINDOW.performance.memory),
  6671. ]),
  6672. );
  6673. } catch (error) {
  6674. // Do nothing
  6675. return [];
  6676. }
  6677. }
  6678. function createMemoryEntry(memoryEntry) {
  6679. const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = memoryEntry;
  6680. // we don't want to use `getAbsoluteTime` because it adds the event time to the
  6681. // time origin, so we get the current timestamp instead
  6682. const time = Date.now() / 1000;
  6683. return {
  6684. type: 'memory',
  6685. name: 'memory',
  6686. start: time,
  6687. end: time,
  6688. data: {
  6689. memory: {
  6690. jsHeapSizeLimit,
  6691. totalJSHeapSize,
  6692. usedJSHeapSize,
  6693. },
  6694. },
  6695. };
  6696. }
  6697. /**
  6698. * Heavily simplified debounce function based on lodash.debounce.
  6699. *
  6700. * This function takes a callback function (@param fun) and delays its invocation
  6701. * by @param wait milliseconds. Optionally, a maxWait can be specified in @param options,
  6702. * which ensures that the callback is invoked at least once after the specified max. wait time.
  6703. *
  6704. * @param func the function whose invocation is to be debounced
  6705. * @param wait the minimum time until the function is invoked after it was called once
  6706. * @param options the options object, which can contain the `maxWait` property
  6707. *
  6708. * @returns the debounced version of the function, which needs to be called at least once to start the
  6709. * debouncing process. Subsequent calls will reset the debouncing timer and, in case @paramfunc
  6710. * was already invoked in the meantime, return @param func's return value.
  6711. * The debounced function has two additional properties:
  6712. * - `flush`: Invokes the debounced function immediately and returns its return value
  6713. * - `cancel`: Cancels the debouncing process and resets the debouncing timer
  6714. */
  6715. function debounce(func, wait, options) {
  6716. let callbackReturnValue;
  6717. let timerId;
  6718. let maxTimerId;
  6719. const maxWait = options && options.maxWait ? Math.max(options.maxWait, wait) : 0;
  6720. function invokeFunc() {
  6721. cancelTimers();
  6722. callbackReturnValue = func();
  6723. return callbackReturnValue;
  6724. }
  6725. function cancelTimers() {
  6726. timerId !== undefined && clearTimeout(timerId);
  6727. maxTimerId !== undefined && clearTimeout(maxTimerId);
  6728. timerId = maxTimerId = undefined;
  6729. }
  6730. function flush() {
  6731. if (timerId !== undefined || maxTimerId !== undefined) {
  6732. return invokeFunc();
  6733. }
  6734. return callbackReturnValue;
  6735. }
  6736. function debounced() {
  6737. if (timerId) {
  6738. clearTimeout(timerId);
  6739. }
  6740. timerId = setTimeout(invokeFunc, wait);
  6741. if (maxWait && maxTimerId === undefined) {
  6742. maxTimerId = setTimeout(invokeFunc, maxWait);
  6743. }
  6744. return callbackReturnValue;
  6745. }
  6746. debounced.cancel = cancelTimers;
  6747. debounced.flush = flush;
  6748. return debounced;
  6749. }
  6750. /**
  6751. * Handler for recording events.
  6752. *
  6753. * Adds to event buffer, and has varying flushing behaviors if the event was a checkout.
  6754. */
  6755. function getHandleRecordingEmit(replay) {
  6756. let hadFirstEvent = false;
  6757. return (event, _isCheckout) => {
  6758. // If this is false, it means session is expired, create and a new session and wait for checkout
  6759. if (!replay.checkAndHandleExpiredSession()) {
  6760. DEBUG_BUILD && logger.warn('[Replay] Received replay event after session expired.');
  6761. return;
  6762. }
  6763. // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
  6764. // We also want to treat the first event as a checkout, so we handle this specifically here
  6765. const isCheckout = _isCheckout || !hadFirstEvent;
  6766. hadFirstEvent = true;
  6767. if (replay.clickDetector) {
  6768. updateClickDetectorForRecordingEvent(replay.clickDetector, event);
  6769. }
  6770. // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
  6771. replay.addUpdate(() => {
  6772. // The session is always started immediately on pageload/init, but for
  6773. // error-only replays, it should reflect the most recent checkout
  6774. // when an error occurs. Clear any state that happens before this current
  6775. // checkout. This needs to happen before `addEvent()` which updates state
  6776. // dependent on this reset.
  6777. if (replay.recordingMode === 'buffer' && isCheckout) {
  6778. replay.setInitialState();
  6779. }
  6780. // If the event is not added (e.g. due to being paused, disabled, or out of the max replay duration),
  6781. // Skip all further steps
  6782. if (!addEventSync(replay, event, isCheckout)) {
  6783. // Return true to skip scheduling a debounced flush
  6784. return true;
  6785. }
  6786. // Different behavior for full snapshots (type=2), ignore other event types
  6787. // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
  6788. if (!isCheckout) {
  6789. return false;
  6790. }
  6791. // Additionally, create a meta event that will capture certain SDK settings.
  6792. // In order to handle buffer mode, this needs to either be done when we
  6793. // receive checkout events or at flush time.
  6794. //
  6795. // `isCheckout` is always true, but want to be explicit that it should
  6796. // only be added for checkouts
  6797. addSettingsEvent(replay, isCheckout);
  6798. // If there is a previousSessionId after a full snapshot occurs, then
  6799. // the replay session was started due to session expiration. The new session
  6800. // is started before triggering a new checkout and contains the id
  6801. // of the previous session. Do not immediately flush in this case
  6802. // to avoid capturing only the checkout and instead the replay will
  6803. // be captured if they perform any follow-up actions.
  6804. if (replay.session && replay.session.previousSessionId) {
  6805. return true;
  6806. }
  6807. // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
  6808. // this should usually be the timestamp of the checkout event, but to be safe...
  6809. if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
  6810. const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
  6811. if (earliestEvent) {
  6812. logInfo(
  6813. `[Replay] Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`,
  6814. replay.getOptions()._experiments.traceInternals,
  6815. );
  6816. replay.session.started = earliestEvent;
  6817. if (replay.getOptions().stickySession) {
  6818. saveSession(replay.session);
  6819. }
  6820. }
  6821. }
  6822. if (replay.recordingMode === 'session') {
  6823. // If the full snapshot is due to an initial load, we will not have
  6824. // a previous session ID. In this case, we want to buffer events
  6825. // for a set amount of time before flushing. This can help avoid
  6826. // capturing replays of users that immediately close the window.
  6827. // This should never reject
  6828. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  6829. void replay.flush();
  6830. }
  6831. return true;
  6832. });
  6833. };
  6834. }
  6835. /**
  6836. * Exported for tests
  6837. */
  6838. function createOptionsEvent(replay) {
  6839. const options = replay.getOptions();
  6840. return {
  6841. type: EventType.Custom,
  6842. timestamp: Date.now(),
  6843. data: {
  6844. tag: 'options',
  6845. payload: {
  6846. shouldRecordCanvas: replay.isRecordingCanvas(),
  6847. sessionSampleRate: options.sessionSampleRate,
  6848. errorSampleRate: options.errorSampleRate,
  6849. useCompressionOption: options.useCompression,
  6850. blockAllMedia: options.blockAllMedia,
  6851. maskAllText: options.maskAllText,
  6852. maskAllInputs: options.maskAllInputs,
  6853. useCompression: replay.eventBuffer ? replay.eventBuffer.type === 'worker' : false,
  6854. networkDetailHasUrls: options.networkDetailAllowUrls.length > 0,
  6855. networkCaptureBodies: options.networkCaptureBodies,
  6856. networkRequestHasHeaders: options.networkRequestHeaders.length > 0,
  6857. networkResponseHasHeaders: options.networkResponseHeaders.length > 0,
  6858. },
  6859. },
  6860. };
  6861. }
  6862. /**
  6863. * Add a "meta" event that contains a simplified view on current configuration
  6864. * options. This should only be included on the first segment of a recording.
  6865. */
  6866. function addSettingsEvent(replay, isCheckout) {
  6867. // Only need to add this event when sending the first segment
  6868. if (!isCheckout || !replay.session || replay.session.segmentId !== 0) {
  6869. return;
  6870. }
  6871. addEventSync(replay, createOptionsEvent(replay), false);
  6872. }
  6873. /**
  6874. * Create a replay envelope ready to be sent.
  6875. * This includes both the replay event, as well as the recording data.
  6876. */
  6877. function createReplayEnvelope(
  6878. replayEvent,
  6879. recordingData,
  6880. dsn,
  6881. tunnel,
  6882. ) {
  6883. return createEnvelope(
  6884. createEventEnvelopeHeaders(replayEvent, getSdkMetadataForEnvelopeHeader(replayEvent), tunnel, dsn),
  6885. [
  6886. [{ type: 'replay_event' }, replayEvent],
  6887. [
  6888. {
  6889. type: 'replay_recording',
  6890. // If string then we need to encode to UTF8, otherwise will have
  6891. // wrong size. TextEncoder has similar browser support to
  6892. // MutationObserver, although it does not accept IE11.
  6893. length:
  6894. typeof recordingData === 'string' ? new TextEncoder().encode(recordingData).length : recordingData.length,
  6895. },
  6896. recordingData,
  6897. ],
  6898. ],
  6899. );
  6900. }
  6901. /**
  6902. * Prepare the recording data ready to be sent.
  6903. */
  6904. function prepareRecordingData({
  6905. recordingData,
  6906. headers,
  6907. }
  6908. ) {
  6909. let payloadWithSequence;
  6910. // XXX: newline is needed to separate sequence id from events
  6911. const replayHeaders = `${JSON.stringify(headers)}
  6912. `;
  6913. if (typeof recordingData === 'string') {
  6914. payloadWithSequence = `${replayHeaders}${recordingData}`;
  6915. } else {
  6916. const enc = new TextEncoder();
  6917. // XXX: newline is needed to separate sequence id from events
  6918. const sequence = enc.encode(replayHeaders);
  6919. // Merge the two Uint8Arrays
  6920. payloadWithSequence = new Uint8Array(sequence.length + recordingData.length);
  6921. payloadWithSequence.set(sequence);
  6922. payloadWithSequence.set(recordingData, sequence.length);
  6923. }
  6924. return payloadWithSequence;
  6925. }
  6926. /**
  6927. * Prepare a replay event & enrich it with the SDK metadata.
  6928. */
  6929. async function prepareReplayEvent({
  6930. client,
  6931. scope,
  6932. replayId: event_id,
  6933. event,
  6934. }
  6935. ) {
  6936. const integrations =
  6937. typeof client._integrations === 'object' && client._integrations !== null && !Array.isArray(client._integrations)
  6938. ? Object.keys(client._integrations)
  6939. : undefined;
  6940. const eventHint = { event_id, integrations };
  6941. if (client.emit) {
  6942. client.emit('preprocessEvent', event, eventHint);
  6943. }
  6944. const preparedEvent = (await prepareEvent(
  6945. client.getOptions(),
  6946. event,
  6947. eventHint,
  6948. scope,
  6949. client,
  6950. getIsolationScope(),
  6951. )) ;
  6952. // If e.g. a global event processor returned null
  6953. if (!preparedEvent) {
  6954. return null;
  6955. }
  6956. // This normally happens in browser client "_prepareEvent"
  6957. // but since we do not use this private method from the client, but rather the plain import
  6958. // we need to do this manually.
  6959. preparedEvent.platform = preparedEvent.platform || 'javascript';
  6960. // extract the SDK name because `client._prepareEvent` doesn't add it to the event
  6961. const metadata = client.getSdkMetadata && client.getSdkMetadata();
  6962. const { name, version } = (metadata && metadata.sdk) || {};
  6963. preparedEvent.sdk = {
  6964. ...preparedEvent.sdk,
  6965. name: name || 'sentry.javascript.unknown',
  6966. version: version || '0.0.0',
  6967. };
  6968. return preparedEvent;
  6969. }
  6970. /**
  6971. * Send replay attachment using `fetch()`
  6972. */
  6973. async function sendReplayRequest({
  6974. recordingData,
  6975. replayId,
  6976. segmentId: segment_id,
  6977. eventContext,
  6978. timestamp,
  6979. session,
  6980. }) {
  6981. const preparedRecordingData = prepareRecordingData({
  6982. recordingData,
  6983. headers: {
  6984. segment_id,
  6985. },
  6986. });
  6987. const { urls, errorIds, traceIds, initialTimestamp } = eventContext;
  6988. const client = getClient();
  6989. const scope = getCurrentScope();
  6990. const transport = client && client.getTransport();
  6991. const dsn = client && client.getDsn();
  6992. if (!client || !transport || !dsn || !session.sampled) {
  6993. return;
  6994. }
  6995. const baseEvent = {
  6996. type: REPLAY_EVENT_NAME,
  6997. replay_start_timestamp: initialTimestamp / 1000,
  6998. timestamp: timestamp / 1000,
  6999. error_ids: errorIds,
  7000. trace_ids: traceIds,
  7001. urls,
  7002. replay_id: replayId,
  7003. segment_id,
  7004. replay_type: session.sampled,
  7005. };
  7006. const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent });
  7007. if (!replayEvent) {
  7008. // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
  7009. client.recordDroppedEvent('event_processor', 'replay', baseEvent);
  7010. logInfo('An event processor returned `null`, will not send event.');
  7011. return;
  7012. }
  7013. /*
  7014. For reference, the fully built event looks something like this:
  7015. {
  7016. "type": "replay_event",
  7017. "timestamp": 1670837008.634,
  7018. "error_ids": [
  7019. "errorId"
  7020. ],
  7021. "trace_ids": [
  7022. "traceId"
  7023. ],
  7024. "urls": [
  7025. "https://example.com"
  7026. ],
  7027. "replay_id": "eventId",
  7028. "segment_id": 3,
  7029. "replay_type": "error",
  7030. "platform": "javascript",
  7031. "event_id": "eventId",
  7032. "environment": "production",
  7033. "sdk": {
  7034. "integrations": [
  7035. "BrowserTracing",
  7036. "Replay"
  7037. ],
  7038. "name": "sentry.javascript.browser",
  7039. "version": "7.25.0"
  7040. },
  7041. "sdkProcessingMetadata": {},
  7042. "contexts": {
  7043. },
  7044. }
  7045. */
  7046. // Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
  7047. // sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
  7048. // 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
  7049. // of this `delete`, lest we miss putting it back in the next time the property is in use.)
  7050. delete replayEvent.sdkProcessingMetadata;
  7051. const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel);
  7052. let response;
  7053. try {
  7054. response = await transport.send(envelope);
  7055. } catch (err) {
  7056. const error = new Error(UNABLE_TO_SEND_REPLAY);
  7057. try {
  7058. // In case browsers don't allow this property to be writable
  7059. // @ts-expect-error This needs lib es2022 and newer
  7060. error.cause = err;
  7061. } catch (e) {
  7062. // nothing to do
  7063. }
  7064. throw error;
  7065. }
  7066. // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
  7067. if (!response) {
  7068. return response;
  7069. }
  7070. // If the status code is invalid, we want to immediately stop & not retry
  7071. if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
  7072. throw new TransportStatusCodeError(response.statusCode);
  7073. }
  7074. const rateLimits = updateRateLimits({}, response);
  7075. if (isRateLimited(rateLimits, 'replay')) {
  7076. throw new RateLimitError(rateLimits);
  7077. }
  7078. return response;
  7079. }
  7080. /**
  7081. * This error indicates that the transport returned an invalid status code.
  7082. */
  7083. class TransportStatusCodeError extends Error {
  7084. constructor(statusCode) {
  7085. super(`Transport returned status code ${statusCode}`);
  7086. }
  7087. }
  7088. /**
  7089. * This error indicates that we hit a rate limit API error.
  7090. */
  7091. class RateLimitError extends Error {
  7092. constructor(rateLimits) {
  7093. super('Rate limit hit');
  7094. this.rateLimits = rateLimits;
  7095. }
  7096. }
  7097. /**
  7098. * Finalize and send the current replay event to Sentry
  7099. */
  7100. async function sendReplay(
  7101. replayData,
  7102. retryConfig = {
  7103. count: 0,
  7104. interval: RETRY_BASE_INTERVAL,
  7105. },
  7106. ) {
  7107. const { recordingData, options } = replayData;
  7108. // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check)
  7109. if (!recordingData.length) {
  7110. return;
  7111. }
  7112. try {
  7113. await sendReplayRequest(replayData);
  7114. return true;
  7115. } catch (err) {
  7116. if (err instanceof TransportStatusCodeError || err instanceof RateLimitError) {
  7117. throw err;
  7118. }
  7119. // Capture error for every failed replay
  7120. setContext('Replays', {
  7121. _retryCount: retryConfig.count,
  7122. });
  7123. if (DEBUG_BUILD && options._experiments && options._experiments.captureExceptions) {
  7124. captureException(err);
  7125. }
  7126. // If an error happened here, it's likely that uploading the attachment
  7127. // failed, we'll can retry with the same events payload
  7128. if (retryConfig.count >= RETRY_MAX_COUNT) {
  7129. const error = new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`);
  7130. try {
  7131. // In case browsers don't allow this property to be writable
  7132. // @ts-expect-error This needs lib es2022 and newer
  7133. error.cause = err;
  7134. } catch (e) {
  7135. // nothing to do
  7136. }
  7137. throw error;
  7138. }
  7139. // will retry in intervals of 5, 10, 30
  7140. retryConfig.interval *= ++retryConfig.count;
  7141. return new Promise((resolve, reject) => {
  7142. setTimeout(async () => {
  7143. try {
  7144. await sendReplay(replayData, retryConfig);
  7145. resolve(true);
  7146. } catch (err) {
  7147. reject(err);
  7148. }
  7149. }, retryConfig.interval);
  7150. });
  7151. }
  7152. }
  7153. const THROTTLED = '__THROTTLED';
  7154. const SKIPPED = '__SKIPPED';
  7155. /**
  7156. * Create a throttled function off a given function.
  7157. * When calling the throttled function, it will call the original function only
  7158. * if it hasn't been called more than `maxCount` times in the last `durationSeconds`.
  7159. *
  7160. * Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`,
  7161. * or else the return value of the original function.
  7162. */
  7163. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  7164. function throttle(
  7165. fn,
  7166. maxCount,
  7167. durationSeconds,
  7168. ) {
  7169. const counter = new Map();
  7170. const _cleanup = (now) => {
  7171. const threshold = now - durationSeconds;
  7172. counter.forEach((_value, key) => {
  7173. if (key < threshold) {
  7174. counter.delete(key);
  7175. }
  7176. });
  7177. };
  7178. const _getTotalCount = () => {
  7179. return [...counter.values()].reduce((a, b) => a + b, 0);
  7180. };
  7181. let isThrottled = false;
  7182. return (...rest) => {
  7183. // Date in second-precision, which we use as basis for the throttling
  7184. const now = Math.floor(Date.now() / 1000);
  7185. // First, make sure to delete any old entries
  7186. _cleanup(now);
  7187. // If already over limit, do nothing
  7188. if (_getTotalCount() >= maxCount) {
  7189. const wasThrottled = isThrottled;
  7190. isThrottled = true;
  7191. return wasThrottled ? SKIPPED : THROTTLED;
  7192. }
  7193. isThrottled = false;
  7194. const count = counter.get(now) || 0;
  7195. counter.set(now, count + 1);
  7196. return fn(...rest);
  7197. };
  7198. }
  7199. /* eslint-disable max-lines */ // TODO: We might want to split this file up
  7200. /**
  7201. * The main replay container class, which holds all the state and methods for recording and sending replays.
  7202. */
  7203. class ReplayContainer {
  7204. /**
  7205. * Recording can happen in one of three modes:
  7206. * - session: Record the whole session, sending it continuously
  7207. * - buffer: Always keep the last 60s of recording, requires:
  7208. * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs
  7209. * - or calling `flush()` to send the replay
  7210. */
  7211. /**
  7212. * The current or last active transcation.
  7213. * This is only available when performance is enabled.
  7214. */
  7215. /**
  7216. * These are here so we can overwrite them in tests etc.
  7217. * @hidden
  7218. */
  7219. /**
  7220. * Options to pass to `rrweb.record()`
  7221. */
  7222. /**
  7223. * Timestamp of the last user activity. This lives across sessions.
  7224. */
  7225. /**
  7226. * Is the integration currently active?
  7227. */
  7228. /**
  7229. * Paused is a state where:
  7230. * - DOM Recording is not listening at all
  7231. * - Nothing will be added to event buffer (e.g. core SDK events)
  7232. */
  7233. /**
  7234. * Have we attached listeners to the core SDK?
  7235. * Note we have to track this as there is no way to remove instrumentation handlers.
  7236. */
  7237. /**
  7238. * Function to stop recording
  7239. */
  7240. /**
  7241. * Internal use for canvas recording options
  7242. */
  7243. constructor({
  7244. options,
  7245. recordingOptions,
  7246. }
  7247. ) {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);
  7248. this.eventBuffer = null;
  7249. this.performanceEntries = [];
  7250. this.replayPerformanceEntries = [];
  7251. this.recordingMode = 'session';
  7252. this.timeouts = {
  7253. sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
  7254. sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
  7255. } ;
  7256. this._lastActivity = Date.now();
  7257. this._isEnabled = false;
  7258. this._isPaused = false;
  7259. this._hasInitializedCoreListeners = false;
  7260. this._context = {
  7261. errorIds: new Set(),
  7262. traceIds: new Set(),
  7263. urls: [],
  7264. initialTimestamp: Date.now(),
  7265. initialUrl: '',
  7266. };
  7267. this._recordingOptions = recordingOptions;
  7268. this._options = options;
  7269. this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, {
  7270. maxWait: this._options.flushMaxDelay,
  7271. });
  7272. this._throttledAddEvent = throttle(
  7273. (event, isCheckout) => addEvent(this, event, isCheckout),
  7274. // Max 300 events...
  7275. 300,
  7276. // ... per 5s
  7277. 5,
  7278. );
  7279. const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions();
  7280. const slowClickConfig = slowClickTimeout
  7281. ? {
  7282. threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
  7283. timeout: slowClickTimeout,
  7284. scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
  7285. ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
  7286. }
  7287. : undefined;
  7288. if (slowClickConfig) {
  7289. this.clickDetector = new ClickDetector(this, slowClickConfig);
  7290. }
  7291. }
  7292. /** Get the event context. */
  7293. getContext() {
  7294. return this._context;
  7295. }
  7296. /** If recording is currently enabled. */
  7297. isEnabled() {
  7298. return this._isEnabled;
  7299. }
  7300. /** If recording is currently paused. */
  7301. isPaused() {
  7302. return this._isPaused;
  7303. }
  7304. /**
  7305. * Determine if canvas recording is enabled
  7306. */
  7307. isRecordingCanvas() {
  7308. return Boolean(this._canvas);
  7309. }
  7310. /** Get the replay integration options. */
  7311. getOptions() {
  7312. return this._options;
  7313. }
  7314. /**
  7315. * Initializes the plugin based on sampling configuration. Should not be
  7316. * called outside of constructor.
  7317. */
  7318. initializeSampling(previousSessionId) {
  7319. const { errorSampleRate, sessionSampleRate } = this._options;
  7320. // If neither sample rate is > 0, then do nothing - user will need to call one of
  7321. // `start()` or `startBuffering` themselves.
  7322. if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
  7323. return;
  7324. }
  7325. // Otherwise if there is _any_ sample rate set, try to load an existing
  7326. // session, or create a new one.
  7327. this._initializeSessionForSampling(previousSessionId);
  7328. if (!this.session) {
  7329. // This should not happen, something wrong has occurred
  7330. this._handleException(new Error('Unable to initialize and create session'));
  7331. return;
  7332. }
  7333. if (this.session.sampled === false) {
  7334. // This should only occur if `errorSampleRate` is 0 and was unsampled for
  7335. // session-based replay. In this case there is nothing to do.
  7336. return;
  7337. }
  7338. // If segmentId > 0, it means we've previously already captured this session
  7339. // In this case, we still want to continue in `session` recording mode
  7340. this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session';
  7341. logInfoNextTick(
  7342. `[Replay] Starting replay in ${this.recordingMode} mode`,
  7343. this._options._experiments.traceInternals,
  7344. );
  7345. this._initializeRecording();
  7346. }
  7347. /**
  7348. * Start a replay regardless of sampling rate. Calling this will always
  7349. * create a new session. Will throw an error if replay is already in progress.
  7350. *
  7351. * Creates or loads a session, attaches listeners to varying events (DOM,
  7352. * _performanceObserver, Recording, Sentry SDK, etc)
  7353. */
  7354. start() {
  7355. if (this._isEnabled && this.recordingMode === 'session') {
  7356. throw new Error('Replay recording is already in progress');
  7357. }
  7358. if (this._isEnabled && this.recordingMode === 'buffer') {
  7359. throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
  7360. }
  7361. logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
  7362. const session = loadOrCreateSession(
  7363. {
  7364. maxReplayDuration: this._options.maxReplayDuration,
  7365. sessionIdleExpire: this.timeouts.sessionIdleExpire,
  7366. traceInternals: this._options._experiments.traceInternals,
  7367. },
  7368. {
  7369. stickySession: this._options.stickySession,
  7370. // This is intentional: create a new session-based replay when calling `start()`
  7371. sessionSampleRate: 1,
  7372. allowBuffering: false,
  7373. },
  7374. );
  7375. this.session = session;
  7376. this._initializeRecording();
  7377. }
  7378. /**
  7379. * Start replay buffering. Buffers until `flush()` is called or, if
  7380. * `replaysOnErrorSampleRate` > 0, an error occurs.
  7381. */
  7382. startBuffering() {
  7383. if (this._isEnabled) {
  7384. throw new Error('Replay recording is already in progress');
  7385. }
  7386. logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
  7387. const session = loadOrCreateSession(
  7388. {
  7389. sessionIdleExpire: this.timeouts.sessionIdleExpire,
  7390. maxReplayDuration: this._options.maxReplayDuration,
  7391. traceInternals: this._options._experiments.traceInternals,
  7392. },
  7393. {
  7394. stickySession: this._options.stickySession,
  7395. sessionSampleRate: 0,
  7396. allowBuffering: true,
  7397. },
  7398. );
  7399. this.session = session;
  7400. this.recordingMode = 'buffer';
  7401. this._initializeRecording();
  7402. }
  7403. /**
  7404. * Start recording.
  7405. *
  7406. * Note that this will cause a new DOM checkout
  7407. */
  7408. startRecording() {
  7409. try {
  7410. const canvasOptions = this._canvas;
  7411. this._stopRecording = record({
  7412. ...this._recordingOptions,
  7413. // When running in error sampling mode, we need to overwrite `checkoutEveryNms`
  7414. // Without this, it would record forever, until an error happens, which we don't want
  7415. // instead, we'll always keep the last 60 seconds of replay before an error happened
  7416. ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }),
  7417. emit: getHandleRecordingEmit(this),
  7418. onMutation: this._onMutationHandler,
  7419. ...(canvasOptions
  7420. ? {
  7421. recordCanvas: canvasOptions.recordCanvas,
  7422. getCanvasManager: canvasOptions.getCanvasManager,
  7423. sampling: canvasOptions.sampling,
  7424. dataURLOptions: canvasOptions.dataURLOptions,
  7425. }
  7426. : {}),
  7427. });
  7428. } catch (err) {
  7429. this._handleException(err);
  7430. }
  7431. }
  7432. /**
  7433. * Stops the recording, if it was running.
  7434. *
  7435. * Returns true if it was previously stopped, or is now stopped,
  7436. * otherwise false.
  7437. */
  7438. stopRecording() {
  7439. try {
  7440. if (this._stopRecording) {
  7441. this._stopRecording();
  7442. this._stopRecording = undefined;
  7443. }
  7444. return true;
  7445. } catch (err) {
  7446. this._handleException(err);
  7447. return false;
  7448. }
  7449. }
  7450. /**
  7451. * Currently, this needs to be manually called (e.g. for tests). Sentry SDK
  7452. * does not support a teardown
  7453. */
  7454. async stop({ forceFlush = false, reason } = {}) {
  7455. if (!this._isEnabled) {
  7456. return;
  7457. }
  7458. // We can't move `_isEnabled` after awaiting a flush, otherwise we can
  7459. // enter into an infinite loop when `stop()` is called while flushing.
  7460. this._isEnabled = false;
  7461. try {
  7462. logInfo(
  7463. `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`,
  7464. this._options._experiments.traceInternals,
  7465. );
  7466. this._removeListeners();
  7467. this.stopRecording();
  7468. this._debouncedFlush.cancel();
  7469. // See comment above re: `_isEnabled`, we "force" a flush, ignoring the
  7470. // `_isEnabled` state of the plugin since it was disabled above.
  7471. if (forceFlush) {
  7472. await this._flush({ force: true });
  7473. }
  7474. // After flush, destroy event buffer
  7475. this.eventBuffer && this.eventBuffer.destroy();
  7476. this.eventBuffer = null;
  7477. // Clear session from session storage, note this means if a new session
  7478. // is started after, it will not have `previousSessionId`
  7479. clearSession(this);
  7480. } catch (err) {
  7481. this._handleException(err);
  7482. }
  7483. }
  7484. /**
  7485. * Pause some replay functionality. See comments for `_isPaused`.
  7486. * This differs from stop as this only stops DOM recording, it is
  7487. * not as thorough of a shutdown as `stop()`.
  7488. */
  7489. pause() {
  7490. if (this._isPaused) {
  7491. return;
  7492. }
  7493. this._isPaused = true;
  7494. this.stopRecording();
  7495. logInfo('[Replay] Pausing replay', this._options._experiments.traceInternals);
  7496. }
  7497. /**
  7498. * Resumes recording, see notes for `pause().
  7499. *
  7500. * Note that calling `startRecording()` here will cause a
  7501. * new DOM checkout.`
  7502. */
  7503. resume() {
  7504. if (!this._isPaused || !this._checkSession()) {
  7505. return;
  7506. }
  7507. this._isPaused = false;
  7508. this.startRecording();
  7509. logInfo('[Replay] Resuming replay', this._options._experiments.traceInternals);
  7510. }
  7511. /**
  7512. * If not in "session" recording mode, flush event buffer which will create a new replay.
  7513. * Unless `continueRecording` is false, the replay will continue to record and
  7514. * behave as a "session"-based replay.
  7515. *
  7516. * Otherwise, queue up a flush.
  7517. */
  7518. async sendBufferedReplayOrFlush({ continueRecording = true } = {}) {
  7519. if (this.recordingMode === 'session') {
  7520. return this.flushImmediate();
  7521. }
  7522. const activityTime = Date.now();
  7523. logInfo('[Replay] Converting buffer to session', this._options._experiments.traceInternals);
  7524. // Allow flush to complete before resuming as a session recording, otherwise
  7525. // the checkout from `startRecording` may be included in the payload.
  7526. // Prefer to keep the error replay as a separate (and smaller) segment
  7527. // than the session replay.
  7528. await this.flushImmediate();
  7529. const hasStoppedRecording = this.stopRecording();
  7530. if (!continueRecording || !hasStoppedRecording) {
  7531. return;
  7532. }
  7533. // To avoid race conditions where this is called multiple times, we check here again that we are still buffering
  7534. if ((this.recordingMode ) === 'session') {
  7535. return;
  7536. }
  7537. // Re-start recording in session-mode
  7538. this.recordingMode = 'session';
  7539. // Once this session ends, we do not want to refresh it
  7540. if (this.session) {
  7541. this._updateUserActivity(activityTime);
  7542. this._updateSessionActivity(activityTime);
  7543. this._maybeSaveSession();
  7544. }
  7545. this.startRecording();
  7546. }
  7547. /**
  7548. * We want to batch uploads of replay events. Save events only if
  7549. * `<flushMinDelay>` milliseconds have elapsed since the last event
  7550. * *OR* if `<flushMaxDelay>` milliseconds have elapsed.
  7551. *
  7552. * Accepts a callback to perform side-effects and returns true to stop batch
  7553. * processing and hand back control to caller.
  7554. */
  7555. addUpdate(cb) {
  7556. // We need to always run `cb` (e.g. in the case of `this.recordingMode == 'buffer'`)
  7557. const cbResult = cb();
  7558. // If this option is turned on then we will only want to call `flush`
  7559. // explicitly
  7560. if (this.recordingMode === 'buffer') {
  7561. return;
  7562. }
  7563. // If callback is true, we do not want to continue with flushing -- the
  7564. // caller will need to handle it.
  7565. if (cbResult === true) {
  7566. return;
  7567. }
  7568. // addUpdate is called quite frequently - use _debouncedFlush so that it
  7569. // respects the flush delays and does not flush immediately
  7570. this._debouncedFlush();
  7571. }
  7572. /**
  7573. * Updates the user activity timestamp and resumes recording. This should be
  7574. * called in an event handler for a user action that we consider as the user
  7575. * being "active" (e.g. a mouse click).
  7576. */
  7577. triggerUserActivity() {
  7578. this._updateUserActivity();
  7579. // This case means that recording was once stopped due to inactivity.
  7580. // Ensure that recording is resumed.
  7581. if (!this._stopRecording) {
  7582. // Create a new session, otherwise when the user action is flushed, it
  7583. // will get rejected due to an expired session.
  7584. if (!this._checkSession()) {
  7585. return;
  7586. }
  7587. // Note: This will cause a new DOM checkout
  7588. this.resume();
  7589. return;
  7590. }
  7591. // Otherwise... recording was never suspended, continue as normalish
  7592. this.checkAndHandleExpiredSession();
  7593. this._updateSessionActivity();
  7594. }
  7595. /**
  7596. * Updates the user activity timestamp *without* resuming
  7597. * recording. Some user events (e.g. keydown) can be create
  7598. * low-value replays that only contain the keypress as a
  7599. * breadcrumb. Instead this would require other events to
  7600. * create a new replay after a session has expired.
  7601. */
  7602. updateUserActivity() {
  7603. this._updateUserActivity();
  7604. this._updateSessionActivity();
  7605. }
  7606. /**
  7607. * Only flush if `this.recordingMode === 'session'`
  7608. */
  7609. conditionalFlush() {
  7610. if (this.recordingMode === 'buffer') {
  7611. return Promise.resolve();
  7612. }
  7613. return this.flushImmediate();
  7614. }
  7615. /**
  7616. * Flush using debounce flush
  7617. */
  7618. flush() {
  7619. return this._debouncedFlush() ;
  7620. }
  7621. /**
  7622. * Always flush via `_debouncedFlush` so that we do not have flushes triggered
  7623. * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
  7624. * cases of mulitple flushes happening closely together.
  7625. */
  7626. flushImmediate() {
  7627. this._debouncedFlush();
  7628. // `.flush` is provided by the debounced function, analogously to lodash.debounce
  7629. return this._debouncedFlush.flush() ;
  7630. }
  7631. /**
  7632. * Cancels queued up flushes.
  7633. */
  7634. cancelFlush() {
  7635. this._debouncedFlush.cancel();
  7636. }
  7637. /** Get the current sesion (=replay) ID */
  7638. getSessionId() {
  7639. return this.session && this.session.id;
  7640. }
  7641. /**
  7642. * Checks if recording should be stopped due to user inactivity. Otherwise
  7643. * check if session is expired and create a new session if so. Triggers a new
  7644. * full snapshot on new session.
  7645. *
  7646. * Returns true if session is not expired, false otherwise.
  7647. * @hidden
  7648. */
  7649. checkAndHandleExpiredSession() {
  7650. // Prevent starting a new session if the last user activity is older than
  7651. // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
  7652. // session+recording. This creates noisy replays that do not have much
  7653. // content in them.
  7654. if (
  7655. this._lastActivity &&
  7656. isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
  7657. this.session &&
  7658. this.session.sampled === 'session'
  7659. ) {
  7660. // Pause recording only for session-based replays. Otherwise, resuming
  7661. // will create a new replay and will conflict with users who only choose
  7662. // to record error-based replays only. (e.g. the resumed replay will not
  7663. // contain a reference to an error)
  7664. this.pause();
  7665. return;
  7666. }
  7667. // --- There is recent user activity --- //
  7668. // This will create a new session if expired, based on expiry length
  7669. if (!this._checkSession()) {
  7670. // Check session handles the refreshing itself
  7671. return false;
  7672. }
  7673. return true;
  7674. }
  7675. /**
  7676. * Capture some initial state that can change throughout the lifespan of the
  7677. * replay. This is required because otherwise they would be captured at the
  7678. * first flush.
  7679. */
  7680. setInitialState() {
  7681. const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
  7682. const url = `${WINDOW.location.origin}${urlPath}`;
  7683. this.performanceEntries = [];
  7684. this.replayPerformanceEntries = [];
  7685. // Reset _context as well
  7686. this._clearContext();
  7687. this._context.initialUrl = url;
  7688. this._context.initialTimestamp = Date.now();
  7689. this._context.urls.push(url);
  7690. }
  7691. /**
  7692. * Add a breadcrumb event, that may be throttled.
  7693. * If it was throttled, we add a custom breadcrumb to indicate that.
  7694. */
  7695. throttledAddEvent(
  7696. event,
  7697. isCheckout,
  7698. ) {
  7699. const res = this._throttledAddEvent(event, isCheckout);
  7700. // If this is THROTTLED, it means we have throttled the event for the first time
  7701. // In this case, we want to add a breadcrumb indicating that something was skipped
  7702. if (res === THROTTLED) {
  7703. const breadcrumb = createBreadcrumb({
  7704. category: 'replay.throttled',
  7705. });
  7706. this.addUpdate(() => {
  7707. // Return `false` if the event _was_ added, as that means we schedule a flush
  7708. return !addEventSync(this, {
  7709. type: ReplayEventTypeCustom,
  7710. timestamp: breadcrumb.timestamp || 0,
  7711. data: {
  7712. tag: 'breadcrumb',
  7713. payload: breadcrumb,
  7714. metric: true,
  7715. },
  7716. });
  7717. });
  7718. }
  7719. return res;
  7720. }
  7721. /**
  7722. * This will get the parametrized route name of the current page.
  7723. * This is only available if performance is enabled, and if an instrumented router is used.
  7724. */
  7725. getCurrentRoute() {
  7726. // eslint-disable-next-line deprecation/deprecation
  7727. const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction();
  7728. const attributes = (lastTransaction && spanToJSON(lastTransaction).data) || {};
  7729. const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
  7730. if (!lastTransaction || !source || !['route', 'custom'].includes(source)) {
  7731. return undefined;
  7732. }
  7733. return spanToJSON(lastTransaction).description;
  7734. }
  7735. /**
  7736. * Initialize and start all listeners to varying events (DOM,
  7737. * Performance Observer, Recording, Sentry SDK, etc)
  7738. */
  7739. _initializeRecording() {
  7740. this.setInitialState();
  7741. // this method is generally called on page load or manually - in both cases
  7742. // we should treat it as an activity
  7743. this._updateSessionActivity();
  7744. this.eventBuffer = createEventBuffer({
  7745. useCompression: this._options.useCompression,
  7746. workerUrl: this._options.workerUrl,
  7747. });
  7748. this._removeListeners();
  7749. this._addListeners();
  7750. // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
  7751. this._isEnabled = true;
  7752. this._isPaused = false;
  7753. this.startRecording();
  7754. }
  7755. /** A wrapper to conditionally capture exceptions. */
  7756. _handleException(error) {
  7757. DEBUG_BUILD && logger.error('[Replay]', error);
  7758. if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) {
  7759. captureException(error);
  7760. }
  7761. }
  7762. /**
  7763. * Loads (or refreshes) the current session.
  7764. */
  7765. _initializeSessionForSampling(previousSessionId) {
  7766. // Whenever there is _any_ error sample rate, we always allow buffering
  7767. // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
  7768. const allowBuffering = this._options.errorSampleRate > 0;
  7769. const session = loadOrCreateSession(
  7770. {
  7771. sessionIdleExpire: this.timeouts.sessionIdleExpire,
  7772. maxReplayDuration: this._options.maxReplayDuration,
  7773. traceInternals: this._options._experiments.traceInternals,
  7774. previousSessionId,
  7775. },
  7776. {
  7777. stickySession: this._options.stickySession,
  7778. sessionSampleRate: this._options.sessionSampleRate,
  7779. allowBuffering,
  7780. },
  7781. );
  7782. this.session = session;
  7783. }
  7784. /**
  7785. * Checks and potentially refreshes the current session.
  7786. * Returns false if session is not recorded.
  7787. */
  7788. _checkSession() {
  7789. // If there is no session yet, we do not want to refresh anything
  7790. // This should generally not happen, but to be safe....
  7791. if (!this.session) {
  7792. return false;
  7793. }
  7794. const currentSession = this.session;
  7795. if (
  7796. shouldRefreshSession(currentSession, {
  7797. sessionIdleExpire: this.timeouts.sessionIdleExpire,
  7798. maxReplayDuration: this._options.maxReplayDuration,
  7799. })
  7800. ) {
  7801. // This should never reject
  7802. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  7803. this._refreshSession(currentSession);
  7804. return false;
  7805. }
  7806. return true;
  7807. }
  7808. /**
  7809. * Refresh a session with a new one.
  7810. * This stops the current session (without forcing a flush, as that would never work since we are expired),
  7811. * and then does a new sampling based on the refreshed session.
  7812. */
  7813. async _refreshSession(session) {
  7814. if (!this._isEnabled) {
  7815. return;
  7816. }
  7817. await this.stop({ reason: 'refresh session' });
  7818. this.initializeSampling(session.id);
  7819. }
  7820. /**
  7821. * Adds listeners to record events for the replay
  7822. */
  7823. _addListeners() {
  7824. try {
  7825. WINDOW.document.addEventListener('visibilitychange', this._handleVisibilityChange);
  7826. WINDOW.addEventListener('blur', this._handleWindowBlur);
  7827. WINDOW.addEventListener('focus', this._handleWindowFocus);
  7828. WINDOW.addEventListener('keydown', this._handleKeyboardEvent);
  7829. if (this.clickDetector) {
  7830. this.clickDetector.addListeners();
  7831. }
  7832. // There is no way to remove these listeners, so ensure they are only added once
  7833. if (!this._hasInitializedCoreListeners) {
  7834. addGlobalListeners(this);
  7835. this._hasInitializedCoreListeners = true;
  7836. }
  7837. } catch (err) {
  7838. this._handleException(err);
  7839. }
  7840. this._performanceCleanupCallback = setupPerformanceObserver(this);
  7841. }
  7842. /**
  7843. * Cleans up listeners that were created in `_addListeners`
  7844. */
  7845. _removeListeners() {
  7846. try {
  7847. WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange);
  7848. WINDOW.removeEventListener('blur', this._handleWindowBlur);
  7849. WINDOW.removeEventListener('focus', this._handleWindowFocus);
  7850. WINDOW.removeEventListener('keydown', this._handleKeyboardEvent);
  7851. if (this.clickDetector) {
  7852. this.clickDetector.removeListeners();
  7853. }
  7854. if (this._performanceCleanupCallback) {
  7855. this._performanceCleanupCallback();
  7856. }
  7857. } catch (err) {
  7858. this._handleException(err);
  7859. }
  7860. }
  7861. /**
  7862. * Handle when visibility of the page content changes. Opening a new tab will
  7863. * cause the state to change to hidden because of content of current page will
  7864. * be hidden. Likewise, moving a different window to cover the contents of the
  7865. * page will also trigger a change to a hidden state.
  7866. */
  7867. __init() {this._handleVisibilityChange = () => {
  7868. if (WINDOW.document.visibilityState === 'visible') {
  7869. this._doChangeToForegroundTasks();
  7870. } else {
  7871. this._doChangeToBackgroundTasks();
  7872. }
  7873. };}
  7874. /**
  7875. * Handle when page is blurred
  7876. */
  7877. __init2() {this._handleWindowBlur = () => {
  7878. const breadcrumb = createBreadcrumb({
  7879. category: 'ui.blur',
  7880. });
  7881. // Do not count blur as a user action -- it's part of the process of them
  7882. // leaving the page
  7883. this._doChangeToBackgroundTasks(breadcrumb);
  7884. };}
  7885. /**
  7886. * Handle when page is focused
  7887. */
  7888. __init3() {this._handleWindowFocus = () => {
  7889. const breadcrumb = createBreadcrumb({
  7890. category: 'ui.focus',
  7891. });
  7892. // Do not count focus as a user action -- instead wait until they focus and
  7893. // interactive with page
  7894. this._doChangeToForegroundTasks(breadcrumb);
  7895. };}
  7896. /** Ensure page remains active when a key is pressed. */
  7897. __init4() {this._handleKeyboardEvent = (event) => {
  7898. handleKeyboardEvent(this, event);
  7899. };}
  7900. /**
  7901. * Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
  7902. */
  7903. _doChangeToBackgroundTasks(breadcrumb) {
  7904. if (!this.session) {
  7905. return;
  7906. }
  7907. const expired = isSessionExpired(this.session, {
  7908. maxReplayDuration: this._options.maxReplayDuration,
  7909. sessionIdleExpire: this.timeouts.sessionIdleExpire,
  7910. });
  7911. if (expired) {
  7912. return;
  7913. }
  7914. if (breadcrumb) {
  7915. this._createCustomBreadcrumb(breadcrumb);
  7916. }
  7917. // Send replay when the page/tab becomes hidden. There is no reason to send
  7918. // replay if it becomes visible, since no actions we care about were done
  7919. // while it was hidden
  7920. // This should never reject
  7921. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  7922. void this.conditionalFlush();
  7923. }
  7924. /**
  7925. * Tasks to run when we consider a page to be visible (via focus and/or visibility)
  7926. */
  7927. _doChangeToForegroundTasks(breadcrumb) {
  7928. if (!this.session) {
  7929. return;
  7930. }
  7931. const isSessionActive = this.checkAndHandleExpiredSession();
  7932. if (!isSessionActive) {
  7933. // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
  7934. // ms, we will re-use the existing session, otherwise create a new
  7935. // session
  7936. logInfo('[Replay] Document has become active, but session has expired');
  7937. return;
  7938. }
  7939. if (breadcrumb) {
  7940. this._createCustomBreadcrumb(breadcrumb);
  7941. }
  7942. }
  7943. /**
  7944. * Update user activity (across session lifespans)
  7945. */
  7946. _updateUserActivity(_lastActivity = Date.now()) {
  7947. this._lastActivity = _lastActivity;
  7948. }
  7949. /**
  7950. * Updates the session's last activity timestamp
  7951. */
  7952. _updateSessionActivity(_lastActivity = Date.now()) {
  7953. if (this.session) {
  7954. this.session.lastActivity = _lastActivity;
  7955. this._maybeSaveSession();
  7956. }
  7957. }
  7958. /**
  7959. * Helper to create (and buffer) a replay breadcrumb from a core SDK breadcrumb
  7960. */
  7961. _createCustomBreadcrumb(breadcrumb) {
  7962. this.addUpdate(() => {
  7963. // This should never reject
  7964. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  7965. this.throttledAddEvent({
  7966. type: EventType.Custom,
  7967. timestamp: breadcrumb.timestamp || 0,
  7968. data: {
  7969. tag: 'breadcrumb',
  7970. payload: breadcrumb,
  7971. },
  7972. });
  7973. });
  7974. }
  7975. /**
  7976. * Observed performance events are added to `this.performanceEntries`. These
  7977. * are included in the replay event before it is finished and sent to Sentry.
  7978. */
  7979. _addPerformanceEntries() {
  7980. const performanceEntries = createPerformanceEntries(this.performanceEntries).concat(this.replayPerformanceEntries);
  7981. this.performanceEntries = [];
  7982. this.replayPerformanceEntries = [];
  7983. return Promise.all(createPerformanceSpans(this, performanceEntries));
  7984. }
  7985. /**
  7986. * Clear _context
  7987. */
  7988. _clearContext() {
  7989. // XXX: `initialTimestamp` and `initialUrl` do not get cleared
  7990. this._context.errorIds.clear();
  7991. this._context.traceIds.clear();
  7992. this._context.urls = [];
  7993. }
  7994. /** Update the initial timestamp based on the buffer content. */
  7995. _updateInitialTimestampFromEventBuffer() {
  7996. const { session, eventBuffer } = this;
  7997. if (!session || !eventBuffer) {
  7998. return;
  7999. }
  8000. // we only ever update this on the initial segment
  8001. if (session.segmentId) {
  8002. return;
  8003. }
  8004. const earliestEvent = eventBuffer.getEarliestTimestamp();
  8005. if (earliestEvent && earliestEvent < this._context.initialTimestamp) {
  8006. this._context.initialTimestamp = earliestEvent;
  8007. }
  8008. }
  8009. /**
  8010. * Return and clear _context
  8011. */
  8012. _popEventContext() {
  8013. const _context = {
  8014. initialTimestamp: this._context.initialTimestamp,
  8015. initialUrl: this._context.initialUrl,
  8016. errorIds: Array.from(this._context.errorIds),
  8017. traceIds: Array.from(this._context.traceIds),
  8018. urls: this._context.urls,
  8019. };
  8020. this._clearContext();
  8021. return _context;
  8022. }
  8023. /**
  8024. * Flushes replay event buffer to Sentry.
  8025. *
  8026. * Performance events are only added right before flushing - this is
  8027. * due to the buffered performance observer events.
  8028. *
  8029. * Should never be called directly, only by `flush`
  8030. */
  8031. async _runFlush() {
  8032. const replayId = this.getSessionId();
  8033. if (!this.session || !this.eventBuffer || !replayId) {
  8034. DEBUG_BUILD && logger.error('[Replay] No session or eventBuffer found to flush.');
  8035. return;
  8036. }
  8037. await this._addPerformanceEntries();
  8038. // Check eventBuffer again, as it could have been stopped in the meanwhile
  8039. if (!this.eventBuffer || !this.eventBuffer.hasEvents) {
  8040. return;
  8041. }
  8042. // Only attach memory event if eventBuffer is not empty
  8043. await addMemoryEntry(this);
  8044. // Check eventBuffer again, as it could have been stopped in the meanwhile
  8045. if (!this.eventBuffer) {
  8046. return;
  8047. }
  8048. // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here
  8049. if (replayId !== this.getSessionId()) {
  8050. return;
  8051. }
  8052. try {
  8053. // This uses the data from the eventBuffer, so we need to call this before `finish()
  8054. this._updateInitialTimestampFromEventBuffer();
  8055. const timestamp = Date.now();
  8056. // Check total duration again, to avoid sending outdated stuff
  8057. // We leave 30s wiggle room to accomodate late flushing etc.
  8058. // This _could_ happen when the browser is suspended during flushing, in which case we just want to stop
  8059. if (timestamp - this._context.initialTimestamp > this._options.maxReplayDuration + 30000) {
  8060. throw new Error('Session is too long, not sending replay');
  8061. }
  8062. const eventContext = this._popEventContext();
  8063. // Always increment segmentId regardless of outcome of sending replay
  8064. const segmentId = this.session.segmentId++;
  8065. this._maybeSaveSession();
  8066. // Note this empties the event buffer regardless of outcome of sending replay
  8067. const recordingData = await this.eventBuffer.finish();
  8068. await sendReplay({
  8069. replayId,
  8070. recordingData,
  8071. segmentId,
  8072. eventContext,
  8073. session: this.session,
  8074. options: this.getOptions(),
  8075. timestamp,
  8076. });
  8077. } catch (err) {
  8078. this._handleException(err);
  8079. // This means we retried 3 times and all of them failed,
  8080. // or we ran into a problem we don't want to retry, like rate limiting.
  8081. // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments
  8082. // This should never reject
  8083. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  8084. this.stop({ reason: 'sendReplay' });
  8085. const client = getClient();
  8086. if (client) {
  8087. client.recordDroppedEvent('send_error', 'replay');
  8088. }
  8089. }
  8090. }
  8091. /**
  8092. * Flush recording data to Sentry. Creates a lock so that only a single flush
  8093. * can be active at a time. Do not call this directly.
  8094. */
  8095. __init5() {this._flush = async ({
  8096. force = false,
  8097. }
  8098. = {}) => {
  8099. if (!this._isEnabled && !force) {
  8100. // This can happen if e.g. the replay was stopped because of exceeding the retry limit
  8101. return;
  8102. }
  8103. if (!this.checkAndHandleExpiredSession()) {
  8104. DEBUG_BUILD && logger.error('[Replay] Attempting to finish replay event after session expired.');
  8105. return;
  8106. }
  8107. if (!this.session) {
  8108. // should never happen, as we would have bailed out before
  8109. return;
  8110. }
  8111. const start = this.session.started;
  8112. const now = Date.now();
  8113. const duration = now - start;
  8114. // A flush is about to happen, cancel any queued flushes
  8115. this._debouncedFlush.cancel();
  8116. // If session is too short, or too long (allow some wiggle room over maxReplayDuration), do not send it
  8117. // This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar
  8118. const tooShort = duration < this._options.minReplayDuration;
  8119. const tooLong = duration > this._options.maxReplayDuration + 5000;
  8120. if (tooShort || tooLong) {
  8121. logInfo(
  8122. `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${
  8123. tooShort ? 'short' : 'long'
  8124. }, not sending replay.`,
  8125. this._options._experiments.traceInternals,
  8126. );
  8127. if (tooShort) {
  8128. this._debouncedFlush();
  8129. }
  8130. return;
  8131. }
  8132. const eventBuffer = this.eventBuffer;
  8133. if (eventBuffer && this.session.segmentId === 0 && !eventBuffer.hasCheckout) {
  8134. logInfo('[Replay] Flushing initial segment without checkout.', this._options._experiments.traceInternals);
  8135. // TODO FN: Evaluate if we want to stop here, or remove this again?
  8136. }
  8137. // this._flushLock acts as a lock so that future calls to `_flush()`
  8138. // will be blocked until this promise resolves
  8139. if (!this._flushLock) {
  8140. this._flushLock = this._runFlush();
  8141. await this._flushLock;
  8142. this._flushLock = undefined;
  8143. return;
  8144. }
  8145. // Wait for previous flush to finish, then call the debounced `_flush()`.
  8146. // It's possible there are other flush requests queued and waiting for it
  8147. // to resolve. We want to reduce all outstanding requests (as well as any
  8148. // new flush requests that occur within a second of the locked flush
  8149. // completing) into a single flush.
  8150. try {
  8151. await this._flushLock;
  8152. } catch (err) {
  8153. DEBUG_BUILD && logger.error(err);
  8154. } finally {
  8155. this._debouncedFlush();
  8156. }
  8157. };}
  8158. /** Save the session, if it is sticky */
  8159. _maybeSaveSession() {
  8160. if (this.session && this._options.stickySession) {
  8161. saveSession(this.session);
  8162. }
  8163. }
  8164. /** Handler for rrweb.record.onMutation */
  8165. __init6() {this._onMutationHandler = (mutations) => {
  8166. const count = mutations.length;
  8167. const mutationLimit = this._options.mutationLimit;
  8168. const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit;
  8169. const overMutationLimit = mutationLimit && count > mutationLimit;
  8170. // Create a breadcrumb if a lot of mutations happen at the same time
  8171. // We can show this in the UI as an information with potential performance improvements
  8172. if (count > mutationBreadcrumbLimit || overMutationLimit) {
  8173. const breadcrumb = createBreadcrumb({
  8174. category: 'replay.mutations',
  8175. data: {
  8176. count,
  8177. limit: overMutationLimit,
  8178. },
  8179. });
  8180. this._createCustomBreadcrumb(breadcrumb);
  8181. }
  8182. // Stop replay if over the mutation limit
  8183. if (overMutationLimit) {
  8184. // This should never reject
  8185. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  8186. this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' });
  8187. return false;
  8188. }
  8189. // `true` means we use the regular mutation handling by rrweb
  8190. return true;
  8191. };}
  8192. }
  8193. function getOption(
  8194. selectors,
  8195. defaultSelectors,
  8196. deprecatedClassOption,
  8197. deprecatedSelectorOption,
  8198. ) {
  8199. const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : [];
  8200. const allSelectors = [
  8201. ...selectors,
  8202. // @deprecated
  8203. ...deprecatedSelectors,
  8204. // sentry defaults
  8205. ...defaultSelectors,
  8206. ];
  8207. // @deprecated
  8208. if (typeof deprecatedClassOption !== 'undefined') {
  8209. // NOTE: No support for RegExp
  8210. if (typeof deprecatedClassOption === 'string') {
  8211. allSelectors.push(`.${deprecatedClassOption}`);
  8212. }
  8213. consoleSandbox(() => {
  8214. // eslint-disable-next-line no-console
  8215. console.warn(
  8216. '[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.',
  8217. );
  8218. });
  8219. }
  8220. return allSelectors.join(',');
  8221. }
  8222. /**
  8223. * Returns privacy related configuration for use in rrweb
  8224. */
  8225. function getPrivacyOptions({
  8226. mask,
  8227. unmask,
  8228. block,
  8229. unblock,
  8230. ignore,
  8231. // eslint-disable-next-line deprecation/deprecation
  8232. blockClass,
  8233. // eslint-disable-next-line deprecation/deprecation
  8234. blockSelector,
  8235. // eslint-disable-next-line deprecation/deprecation
  8236. maskTextClass,
  8237. // eslint-disable-next-line deprecation/deprecation
  8238. maskTextSelector,
  8239. // eslint-disable-next-line deprecation/deprecation
  8240. ignoreClass,
  8241. }) {
  8242. const defaultBlockedElements = ['base[href="/"]'];
  8243. const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector);
  8244. const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']);
  8245. const options = {
  8246. // We are making the decision to make text and input selectors the same
  8247. maskTextSelector: maskSelector,
  8248. unmaskTextSelector: unmaskSelector,
  8249. blockSelector: getOption(
  8250. block,
  8251. ['.sentry-block', '[data-sentry-block]', ...defaultBlockedElements],
  8252. blockClass,
  8253. blockSelector,
  8254. ),
  8255. unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']),
  8256. ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]', 'input[type="file"]'], ignoreClass),
  8257. };
  8258. if (blockClass instanceof RegExp) {
  8259. options.blockClass = blockClass;
  8260. }
  8261. if (maskTextClass instanceof RegExp) {
  8262. options.maskTextClass = maskTextClass;
  8263. }
  8264. return options;
  8265. }
  8266. /**
  8267. * Masks an attribute if necessary, otherwise return attribute value as-is.
  8268. */
  8269. function maskAttribute({
  8270. el,
  8271. key,
  8272. maskAttributes,
  8273. maskAllText,
  8274. privacyOptions,
  8275. value,
  8276. }) {
  8277. // We only mask attributes if `maskAllText` is true
  8278. if (!maskAllText) {
  8279. return value;
  8280. }
  8281. // unmaskTextSelector takes precendence
  8282. if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) {
  8283. return value;
  8284. }
  8285. if (
  8286. maskAttributes.includes(key) ||
  8287. // Need to mask `value` attribute for `<input>` if it's a button-like
  8288. // type
  8289. (key === 'value' && el.tagName === 'INPUT' && ['submit', 'button'].includes(el.getAttribute('type') || ''))
  8290. ) {
  8291. return value.replace(/[\S]/g, '*');
  8292. }
  8293. return value;
  8294. }
  8295. const MEDIA_SELECTORS =
  8296. 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]';
  8297. const DEFAULT_NETWORK_HEADERS = ['content-length', 'content-type', 'accept'];
  8298. let _initialized = false;
  8299. const replayIntegration = ((options) => {
  8300. // eslint-disable-next-line deprecation/deprecation
  8301. return new Replay(options);
  8302. }) ;
  8303. /**
  8304. * The main replay integration class, to be passed to `init({ integrations: [] })`.
  8305. * @deprecated Use `replayIntegration()` instead.
  8306. */
  8307. class Replay {
  8308. /**
  8309. * @inheritDoc
  8310. */
  8311. static __initStatic() {this.id = 'Replay';}
  8312. /**
  8313. * @inheritDoc
  8314. */
  8315. /**
  8316. * Options to pass to `rrweb.record()`
  8317. */
  8318. /**
  8319. * Initial options passed to the replay integration, merged with default values.
  8320. * Note: `sessionSampleRate` and `errorSampleRate` are not required here, as they
  8321. * can only be finally set when setupOnce() is called.
  8322. *
  8323. * @private
  8324. */
  8325. constructor({
  8326. flushMinDelay = DEFAULT_FLUSH_MIN_DELAY,
  8327. flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY,
  8328. minReplayDuration = MIN_REPLAY_DURATION,
  8329. maxReplayDuration = MAX_REPLAY_DURATION,
  8330. stickySession = true,
  8331. useCompression = true,
  8332. workerUrl,
  8333. _experiments = {},
  8334. sessionSampleRate,
  8335. errorSampleRate,
  8336. maskAllText = true,
  8337. maskAllInputs = true,
  8338. blockAllMedia = true,
  8339. mutationBreadcrumbLimit = 750,
  8340. mutationLimit = 10000,
  8341. slowClickTimeout = 7000,
  8342. slowClickIgnoreSelectors = [],
  8343. networkDetailAllowUrls = [],
  8344. networkDetailDenyUrls = [],
  8345. networkCaptureBodies = true,
  8346. networkRequestHeaders = [],
  8347. networkResponseHeaders = [],
  8348. mask = [],
  8349. maskAttributes = ['title', 'placeholder'],
  8350. unmask = [],
  8351. block = [],
  8352. unblock = [],
  8353. ignore = [],
  8354. maskFn,
  8355. beforeAddRecordingEvent,
  8356. beforeErrorSampling,
  8357. // eslint-disable-next-line deprecation/deprecation
  8358. blockClass,
  8359. // eslint-disable-next-line deprecation/deprecation
  8360. blockSelector,
  8361. // eslint-disable-next-line deprecation/deprecation
  8362. maskInputOptions,
  8363. // eslint-disable-next-line deprecation/deprecation
  8364. maskTextClass,
  8365. // eslint-disable-next-line deprecation/deprecation
  8366. maskTextSelector,
  8367. // eslint-disable-next-line deprecation/deprecation
  8368. ignoreClass,
  8369. } = {}) {
  8370. // eslint-disable-next-line deprecation/deprecation
  8371. this.name = Replay.id;
  8372. const privacyOptions = getPrivacyOptions({
  8373. mask,
  8374. unmask,
  8375. block,
  8376. unblock,
  8377. ignore,
  8378. blockClass,
  8379. blockSelector,
  8380. maskTextClass,
  8381. maskTextSelector,
  8382. ignoreClass,
  8383. });
  8384. this._recordingOptions = {
  8385. maskAllInputs,
  8386. maskAllText,
  8387. maskInputOptions: { ...(maskInputOptions || {}), password: true },
  8388. maskTextFn: maskFn,
  8389. maskInputFn: maskFn,
  8390. maskAttributeFn: (key, value, el) =>
  8391. maskAttribute({
  8392. maskAttributes,
  8393. maskAllText,
  8394. privacyOptions,
  8395. key,
  8396. value,
  8397. el,
  8398. }),
  8399. ...privacyOptions,
  8400. // Our defaults
  8401. slimDOMOptions: 'all',
  8402. inlineStylesheet: true,
  8403. // Disable inline images as it will increase segment/replay size
  8404. inlineImages: false,
  8405. // collect fonts, but be aware that `sentry.io` needs to be an allowed
  8406. // origin for playback
  8407. collectFonts: true,
  8408. errorHandler: (err) => {
  8409. try {
  8410. err.__rrweb__ = true;
  8411. } catch (error) {
  8412. // ignore errors here
  8413. // this can happen if the error is frozen or does not allow mutation for other reasons
  8414. }
  8415. },
  8416. };
  8417. this._initialOptions = {
  8418. flushMinDelay,
  8419. flushMaxDelay,
  8420. minReplayDuration: Math.min(minReplayDuration, MIN_REPLAY_DURATION_LIMIT),
  8421. maxReplayDuration: Math.min(maxReplayDuration, MAX_REPLAY_DURATION),
  8422. stickySession,
  8423. sessionSampleRate,
  8424. errorSampleRate,
  8425. useCompression,
  8426. workerUrl,
  8427. blockAllMedia,
  8428. maskAllInputs,
  8429. maskAllText,
  8430. mutationBreadcrumbLimit,
  8431. mutationLimit,
  8432. slowClickTimeout,
  8433. slowClickIgnoreSelectors,
  8434. networkDetailAllowUrls,
  8435. networkDetailDenyUrls,
  8436. networkCaptureBodies,
  8437. networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
  8438. networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders),
  8439. beforeAddRecordingEvent,
  8440. beforeErrorSampling,
  8441. _experiments,
  8442. };
  8443. if (typeof sessionSampleRate === 'number') {
  8444. // eslint-disable-next-line
  8445. console.warn(
  8446. `[Replay] You are passing \`sessionSampleRate\` to the Replay integration.
  8447. This option is deprecated and will be removed soon.
  8448. Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options, e.g.:
  8449. Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`,
  8450. );
  8451. this._initialOptions.sessionSampleRate = sessionSampleRate;
  8452. }
  8453. if (typeof errorSampleRate === 'number') {
  8454. // eslint-disable-next-line
  8455. console.warn(
  8456. `[Replay] You are passing \`errorSampleRate\` to the Replay integration.
  8457. This option is deprecated and will be removed soon.
  8458. Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options, e.g.:
  8459. Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
  8460. );
  8461. this._initialOptions.errorSampleRate = errorSampleRate;
  8462. }
  8463. if (this._initialOptions.blockAllMedia) {
  8464. // `blockAllMedia` is a more user friendly option to configure blocking
  8465. // embedded media elements
  8466. this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector
  8467. ? MEDIA_SELECTORS
  8468. : `${this._recordingOptions.blockSelector},${MEDIA_SELECTORS}`;
  8469. }
  8470. if (this._isInitialized && isBrowser()) {
  8471. throw new Error('Multiple Sentry Session Replay instances are not supported');
  8472. }
  8473. this._isInitialized = true;
  8474. }
  8475. /** If replay has already been initialized */
  8476. get _isInitialized() {
  8477. return _initialized;
  8478. }
  8479. /** Update _isInitialized */
  8480. set _isInitialized(value) {
  8481. _initialized = value;
  8482. }
  8483. /**
  8484. * Setup and initialize replay container
  8485. */
  8486. setupOnce() {
  8487. if (!isBrowser()) {
  8488. return;
  8489. }
  8490. this._setup();
  8491. // Once upon a time, we tried to create a transaction in `setupOnce` and it would
  8492. // potentially create a transaction before some native SDK integrations have run
  8493. // and applied their own global event processor. An example is:
  8494. // https://github.com/getsentry/sentry-javascript/blob/b47ceafbdac7f8b99093ce6023726ad4687edc48/packages/browser/src/integrations/useragent.ts
  8495. //
  8496. // So we call `this._initialize()` in next event loop as a workaround to wait for other
  8497. // global event processors to finish. This is no longer needed, but keeping it
  8498. // here to avoid any future issues.
  8499. setTimeout(() => this._initialize());
  8500. }
  8501. /**
  8502. * Start a replay regardless of sampling rate. Calling this will always
  8503. * create a new session. Will throw an error if replay is already in progress.
  8504. *
  8505. * Creates or loads a session, attaches listeners to varying events (DOM,
  8506. * PerformanceObserver, Recording, Sentry SDK, etc)
  8507. */
  8508. start() {
  8509. if (!this._replay) {
  8510. return;
  8511. }
  8512. this._replay.start();
  8513. }
  8514. /**
  8515. * Start replay buffering. Buffers until `flush()` is called or, if
  8516. * `replaysOnErrorSampleRate` > 0, until an error occurs.
  8517. */
  8518. startBuffering() {
  8519. if (!this._replay) {
  8520. return;
  8521. }
  8522. this._replay.startBuffering();
  8523. }
  8524. /**
  8525. * Currently, this needs to be manually called (e.g. for tests). Sentry SDK
  8526. * does not support a teardown
  8527. */
  8528. stop() {
  8529. if (!this._replay) {
  8530. return Promise.resolve();
  8531. }
  8532. return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' });
  8533. }
  8534. /**
  8535. * If not in "session" recording mode, flush event buffer which will create a new replay.
  8536. * Unless `continueRecording` is false, the replay will continue to record and
  8537. * behave as a "session"-based replay.
  8538. *
  8539. * Otherwise, queue up a flush.
  8540. */
  8541. flush(options) {
  8542. if (!this._replay || !this._replay.isEnabled()) {
  8543. return Promise.resolve();
  8544. }
  8545. return this._replay.sendBufferedReplayOrFlush(options);
  8546. }
  8547. /**
  8548. * Get the current session ID.
  8549. */
  8550. getReplayId() {
  8551. if (!this._replay || !this._replay.isEnabled()) {
  8552. return;
  8553. }
  8554. return this._replay.getSessionId();
  8555. }
  8556. /**
  8557. * Initializes replay.
  8558. */
  8559. _initialize() {
  8560. if (!this._replay) {
  8561. return;
  8562. }
  8563. // We have to run this in _initialize, because this runs in setTimeout
  8564. // So when this runs all integrations have been added
  8565. // Before this, we cannot access integrations on the client,
  8566. // so we need to mutate the options here
  8567. this._maybeLoadFromReplayCanvasIntegration();
  8568. this._replay.initializeSampling();
  8569. }
  8570. /** Setup the integration. */
  8571. _setup() {
  8572. // Client is not available in constructor, so we need to wait until setupOnce
  8573. const finalOptions = loadReplayOptionsFromClient(this._initialOptions);
  8574. this._replay = new ReplayContainer({
  8575. options: finalOptions,
  8576. recordingOptions: this._recordingOptions,
  8577. });
  8578. }
  8579. /** Get canvas options from ReplayCanvas integration, if it is also added. */
  8580. _maybeLoadFromReplayCanvasIntegration() {
  8581. // To save bundle size, we skip checking for stuff here
  8582. // and instead just try-catch everything - as generally this should all be defined
  8583. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  8584. try {
  8585. const client = getClient();
  8586. const canvasIntegration = client.getIntegrationByName('ReplayCanvas')
  8587. ;
  8588. if (!canvasIntegration) {
  8589. return;
  8590. }
  8591. this._replay['_canvas'] = canvasIntegration.getOptions();
  8592. } catch (e) {
  8593. // ignore errors here
  8594. }
  8595. /* eslint-enable @typescript-eslint/no-non-null-assertion */
  8596. }
  8597. }Replay.__initStatic();
  8598. /** Parse Replay-related options from SDK options */
  8599. function loadReplayOptionsFromClient(initialOptions) {
  8600. const client = getClient();
  8601. const opt = client && (client.getOptions() );
  8602. const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...dropUndefinedKeys(initialOptions) };
  8603. if (!opt) {
  8604. consoleSandbox(() => {
  8605. // eslint-disable-next-line no-console
  8606. console.warn('SDK client is not available.');
  8607. });
  8608. return finalOptions;
  8609. }
  8610. if (
  8611. initialOptions.sessionSampleRate == null && // TODO remove once deprecated rates are removed
  8612. initialOptions.errorSampleRate == null && // TODO remove once deprecated rates are removed
  8613. opt.replaysSessionSampleRate == null &&
  8614. opt.replaysOnErrorSampleRate == null
  8615. ) {
  8616. consoleSandbox(() => {
  8617. // eslint-disable-next-line no-console
  8618. console.warn(
  8619. 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.',
  8620. );
  8621. });
  8622. }
  8623. if (typeof opt.replaysSessionSampleRate === 'number') {
  8624. finalOptions.sessionSampleRate = opt.replaysSessionSampleRate;
  8625. }
  8626. if (typeof opt.replaysOnErrorSampleRate === 'number') {
  8627. finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate;
  8628. }
  8629. return finalOptions;
  8630. }
  8631. function _getMergedNetworkHeaders(headers) {
  8632. return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())];
  8633. }
  8634. /**
  8635. * This is a small utility to get a type-safe instance of the Replay integration.
  8636. */
  8637. // eslint-disable-next-line deprecation/deprecation
  8638. function getReplay() {
  8639. const client = getClient();
  8640. return (
  8641. client && client.getIntegrationByName && client.getIntegrationByName('Replay')
  8642. );
  8643. }
  8644. export { Replay, getReplay, replayIntegration };
  8645. //# sourceMappingURL=index.js.map