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