1// "use strict"; 2/***********************************//* Init */window.PP=window.PP||{};window.PP.utils=window.PP.utils||{};window.PP.config=window.PP.config||{};window.PP.funnel=window.PP.funnel||{};window.PP.funnel.newsletters=window.PP.funnel.newsletters||{};Object.assign(window.PP.funnel,{F_STATE_USER1:1,F_STATE_USER2:2,F_STATE_USER3:3});Object.assign(window.PP.funnel.newsletters,{//////////////////// 3// newsletter signup is a bitmask: 4"The Big Story":1,// 2^0 5"The Daily Digest":2,// 2^1 6"Weekly Dispatch":4,// 2^2, etc 7"ProPublica Illinois":8,"Guide to Democracy":16,"ProPublica Espa\xF1ol":32,"The Data Newsletter":64,"Not Shutting Up":128,"Job and Internship Opportunities":256,"Local Reporting Network":512,Unknown:1024});window.PP.funnel.modules=window.PP.funnel.modules||{};window.PP.funnel.current_state=window.PP.funnel.current_state||{userLvlOverride:null,newslettersMask:0,sessions:{},lastView:0,// 0 or date of last pageview 8displayModal:true};// ^ this is our "default" empty session state 9window.PP.funnel.get_current_level=function(){// turn the current state {} (`funnel.current_state`) into an 10// integer identifier of which level we are in 11const current_state=window.PP.funnel.current_state;const legitLvls=[PP.funnel.F_STATE_USER1,PP.funnel.F_STATE_USER2,PP.funnel.F_STATE_USER3];// if we're overriding user level on this req with a querystring 12// like `?__uflvl=2` (see `load_qs_override`) 13if(current_state.userLvlOverride!=null&&legitLvls.includes(current_state.userLvlOverride)){return current_state.userLvlOverride}// definitions in propublica/propublica.org-craftv3#759 14if(window.PP.funnel.get_session_count()>=4||window.PP.funnel.get_newsletter_signup_count()>=3){return window.PP.funnel.F_STATE_USER3}else if(window.PP.funnel.get_newsletter_signup_count()>0){return window.PP.funnel.F_STATE_USER2}else{// default 15return window.PP.funnel.F_STATE_USER1}};window.PP.funnel.run_cookie_check=function(){};/***********************************//* Funnel functionality: date utilities */window.PP.funnel.date_encode=function(dateobj){// turn Date() into `YYMMDD` string 16let mon=dateobj.getMonth()+1;if(mon<10){mon="0"+mon}let day=dateobj.getDate();if(day<10){day="0"+day}// 2 digit year string ("21" for 2021) 17let yr=(""+dateobj.getFullYear()).slice(2,4);return""+yr+""+mon+""+day};window.PP.funnel.date_decode=function(datestr){// turn `YYMMDD` string into Date object 18return new Date("20"+datestr.slice(0,2),datestr.slice(2,4)-1,datestr.slice(4,6))};window.PP.funnel.date_compress=function(dateobj){// turn Date() into a six digit number (`YYMMDD`), and then 19// compresses that integer into a base-36 string 20const dateInt=// get each part as 2 digits & shift yr & month over 21(dateobj.getFullYear()-2000)*10000+// lol if this code is being used next century 22(dateobj.getMonth()+1)*100+// getMonth is 0-indexed (jan = 0) 23dateobj.getDate();// but days isn't 0-indexed ¯\_(ã)_/¯ 24let dateIntEnc=dateInt.toString(36);return dateIntEnc};window.PP.funnel.date_decompress=function(datestr){// turn base-36 number (from date_compress) back into a Date obj 25const dateInt=parseInt(datestr,36);// all these do is shift the digit pairs back out: 26const year2=parseInt(dateInt/10000,10);const month=parseInt(dateInt%10000/100,10);const day=dateInt%100;return new Date(2000+year2,month-1,day)};window.PP.funnel.ts_compress=function(dateObj){// turn Date() into a six digit number (`YYMMDDHHMM`), and then 27// compresses that integer into a base-36 string 28const dateInt=// get each part as 2 digits & shift yr & month over 29(dateObj.getFullYear()-2000)*100000000+// lol if this code is being used next century 30(dateObj.getMonth()+1)*1000000+// getMonth is 0-indexed (jan = 0) 31dateObj.getDate()*10000+// but days isn't 0-indexed ¯\_(ã)_/¯ 32dateObj.getHours()*100+dateObj.getMinutes();return dateInt.toString(36)};window.PP.funnel.ts_decompress=function(datestr){// turn base-36 number (from date_compress) back into a Date obj 33const dateInt=parseInt(datestr,36);// all these do is shift the digit pairs back out: 34const year2=parseInt(dateInt/100000000,10);const month=parseInt(dateInt%100000000/1000000,10);const day=parseInt(dateInt%1000000/10000,10);const hours=parseInt(dateInt%10000/100,10);const minutes=dateInt%100;return new Date(2000+year2,month-1,day,hours,minutes)};window.PP.funnel.date_daydelta=function(dateObjA,dateObjB){// given two Date objs, # of days between them. (rounded down; integer.) 35return Math.floor(Math.abs(Date.UTC(dateObjB.getFullYear(),dateObjB.getMonth(),dateObjB.getDate())-Date.UTC(dateObjA.getFullYear(),dateObjA.getMonth(),dateObjA.getDate()))/86400000)};window.PP.funnel.date_add_days=function(dateobj,days){// given a Date object, returns a new Date object adding `days` 36// to it
37const ret=new Date(dateobj.getTime());ret.setDate(dateobj.getDate()+days);// PP.utils.log(ret); 38return ret};window.PP.funnel.date_start_of_wk=function(d){// given d (date obj), returns a date obj of the most recent sunday 39var t=new Date(d);// getDay is the 0-6 day of the week, so cleverly enough if we subtract 40// that many days from the current day, this always lands on sunday. 41// (and setDate handles things like "december -2" -> "november 29") 42t.setDate(t.getDate()-t.getDay());return t};/***********************************//* Funnel functionality: data handlers (encode/decode into cookie) */window.PP.funnel.state_encode=function(funnelState){// Encodes some user visit information into a compact string (usable in 43// a cookie). Value is pipe-delimited, and first field is the version 44// of this cookie that's encoded. (Current latest: 0) 45// version|... 46// version 0: 47// 0|displayModal|newsletterMask|lastView|firstCountDate|session_counts... 48// 49// - version 50// - `displayModal` boolean for displaying overlay modal 51// - signups -> bitmask 52// - `lastView` is a Date for most recent time user viewed a page 53// - `firstCountDate` is the Date representing the earliest week we have a session count 54// - `session_counts` is encoded as D=CT 55// where D is # of days since `firstDate` (rather than date, to keep cookie small) 56// and CT is num_sessions on that day 57let PP=window.PP;// PP.utils.log(funnelState); 58const today=new Date;const version="0";const displayModal=funnelState.displayModal?1:0;// not gonna use this for now, but keep the field around 59// make sure our `newsletter_signup` value is a bool 60let newslettersMask=funnelState["newslettersMask"]||0;let newslettersMaskEnc=newslettersMask.toString(36);let lastView="0";if(!!funnelState.lastView){lastView=PP.funnel.ts_compress(funnelState.lastView)}const sessions=funnelState.sessions||{};var ret=""+version+"|"+displayModal+"|"+newslettersMaskEnc+"|"+lastView;var firstCountDate=null;// loop over the keys of `.sessions` in order 61Object.keys(sessions).sort().forEach(dstamp=>{if(PP.funnel.date_daydelta(PP.funnel.date_decode(dstamp),today)>90){// silently drop counts older than 90 days 62return}if(firstCountDate===null){// we've hit the first valid date in `sessions`, so save this as 63// our starting day 64firstCountDate=PP.funnel.date_decode(dstamp);const compressedDate=PP.funnel.date_compress(firstCountDate);ret+="|"+compressedDate}const diff=PP.funnel.date_daydelta(firstCountDate,PP.funnel.date_decode(dstamp));// we convert the 0-90 value into base 36 (0-2i) since it means 65// we can encode 0 thru 35 in 1 character (35=z) 66const diffB36=diff.toString(36);// https://stackoverflow.com/a/57805 67ret+="|"+diffB36+"="+(sessions[dstamp]||0)});// PP.utils.log(ret); 68return ret};// See `state_encode`. Decodes data from `state_encode`-encoded string and returns 69// an object. For version 0, looks like: 70// { 71// did_newsletter_signup: false, // true or false 72// sessions: { 73// 20201201: 4 // for each date: key is "YYYYMMDD", value is num_sessions 74// ... 75// } 76// } 77window.PP.funnel.state_decode=function(cval){let PP=window.PP;// PP.utils.log(cval.split("|")); 78const today=new Date;const bits=cval.split("|");const ret={};if(bits[0]==="0"){// 0|0|newslettersMask|lastView|firstCountDate|session_counts... 79ret["newslettersMask"]=parseInt(bits[2],36)||0;ret["displayModal"]=bits[1]==="0"?false:true;if(bits[3]==="0"){ret["lastView"]=0}else{ret["lastView"]=PP.funnel.ts_decompress(bits[3])}ret["sessions"]={};if(!bits[4]||bits.slice(5).length===0){// the current cookie actually has no sessions 80}else{const firstDate=PP.funnel.date_decompress(bits[4]);bits.slice(5).forEach(rawDayCount=>{const dayCount=rawDayCount.split("=");// we are encoding date delta (# days since `firstDate`) 81// as base36, so expand that to a normal int 82const dayOffset=parseInt(dayCount[0],36);// https://stackoverflow.com/a/57805 83const sessionCount=parseInt(dayCount[1],10);// PP.utils.log('dayOffset '+dayOffset); 84// PP.utils.log('sessionCount '+sessionCount); 85const realDateStr=PP.funnel.date_encode(PP.funnel.date_add_days(firstDate,dayOffset));ret.sessions[realDateStr]=sessionCount;// PP.utils.log(ret['first_session']); 86// PP.utils.log(parseInt(ct, 10)); 87// PP.utils.log(); 88});// after importing all that data, parse through and limit our 89// counters to 90 days
90Object.keys(ret.sessions).forEach(dateStr=>{if(PP.funnel.date_daydelta(today,PP.funnel.date_decode(dateStr))>90){delete ret.sessions[dateStr]}})}return ret}else{return undefined}};// cleans out values from the `funnel.current_state.sessions` object that are 91// more than 13 weeks (90 days) old 92window.PP.funnel.clean_current_state=function(){let PP=window.PP;const today=new Date;Object.keys(PP.funnel.current_state.sessions).sort().forEach(d=>{// drop any "session" value over >90 days old (past 13th week) 93if(PP.funnel.date_daydelta(today,PP.funnel.date_decode(d))>90){delete PP.funnel.current_state.sessions[d]}})};window.PP.funnel.add_newsletter_signup=function(newsletter){if(newsletter=="All Emails"){return}let fun=window.PP.funnel;if(!Object.keys(fun.newsletters).includes(newsletter)){newsletter="Unknown"}const newsletterMaskId=fun.newsletters[newsletter];fun.current_state.newslettersMask|=newsletterMaskId};window.PP.funnel.is_signedup=function(newsletter){const mask=window.PP.funnel.current_state.newslettersMask;let fun=window.PP.funnel;if(!!(mask&fun.newsletters[newsletter])){return true}else{return false}};window.PP.funnel.get_newsletter_signup_count=function(){const mask=window.PP.funnel.current_state.newslettersMask;let fun=window.PP.funnel;let newsletterCounter=0;Object.keys(fun.newsletters).forEach(newsletter=>{if(!!(mask&fun.newsletters[newsletter])){newsletterCounter+=1}});return newsletterCounter};window.PP.funnel.get_newsletter_signups=function(){const mask=window.PP.funnel.current_state.newslettersMask;let fun=window.PP.funnel;let newsletters="";Object.keys(fun.newsletters).forEach(newsletter=>{if(!!(mask&fun.newsletters[newsletter])){newsletters+=newsletter+", "}});if(newsletters.length>0){newsletters=newsletters.substring(0,newsletters.length-2)}return newsletters};// Number of sessions user has in the past 90 days 94window.PP.funnel.get_session_count=function(){let PP=window.PP;PP.funnel.clean_current_state();let counter=0;if(!PP.funnel.current_state){return counter}if(!PP.funnel.current_state.sessions){return counter}
94Object.keys(PP.funnel.current_state.sessions).forEach(d=>{// since we do the 90 day filter on state_encode and state_decode, we 95// don't need to do it again here 96counter+=PP.funnel.current_state.sessions[d]});return counter};window.PP.funnel.try_add_session=function(){let PP=window.PP;if(!PP.funnel.current_state){return}const now=new Date;const nowTrimmed=new Date(now.getFullYear(),now.getMonth(),now.getDate(),now.getHours(),now.getMinutes());const thisWeek=PP.funnel.date_start_of_wk(now);const thisWeekStr=PP.funnel.date_encode(thisWeek);if(!!PP.funnel.current_state.lastView){// we're going to use the definition of a new session that (according 97// to https://swizec.com/blog/counting-web-sessions-with-javascript ) 98// is used by google analytics: a new session is triggered by: 99// * 30 minutes of inactivity 100// * midnight 101// calculate diff in minutes between now & lastView 102// https://bearnithi.com/2019/11/10/how-to-calculate-the-time-difference-days-hours-minutes-between-two-dates-in-javascript/ 103const lastView=PP.funnel.current_state.lastView;// 1 min = 60k milliseconds, so we count the diff in that 104const minuteDiff=Math.floor(Math.abs(nowTrimmed-lastView)/60000)%60000;const isDifferentDay=// we have crossed midnight at least once 105now.getFullYear()>lastView.getFullYear()||now.getMonth()>lastView.getMonth()||now.getDate()>lastView.getDate();PP.utils.log("is_differnt_day: "+isDifferentDay);PP.utils.log("minutes since last PV: "+minuteDiff);if(minuteDiff>=30||isDifferentDay){// add a new session to our counter for this week 106PP.utils.log("incrementing session counter");if(!!PP.funnel.current_state.sessions[thisWeekStr]){PP.funnel.current_state.sessions[thisWeekStr]+=1}else{PP.funnel.current_state.sessions[thisWeekStr]=1}PP.funnel.current_state.displayModal=true}}else{// this is a fresh cookie? 107// init this week's session counter to 1 108PP.funnel.current_state.sessions[thisWeekStr]=1;// const cmp = PP.funnel.ts_compress(PP.funnel.current_state.lastView) 109// PP.utils.log(cmp); 110// PP.utils.log(PP.funnel.ts_decompress(cmp)); 111}// update the "lastView" (as long as user is active we keep 112// it fresh; if it gets too stale _that_ is when we increment) 113PP.utils.log("updating lastView timestamp");PP.funnel.current_state.lastView=nowTrimmed};window.PP.funnel.load_cookie=function(){let PP=window.PP;const cookie=PP.utils.cookie("ppfun");if(!!cookie){window.PP.funnel.current_state=window.PP.funnel.state_decode(cookie);return cookie}else{return false}};window.PP.funnel.load_qs_override=function(){function getParameterByName(name){const regex=new RegExp(`[\\?&]${name}=([^&#]*)`);const results=regex.exec(window.location.search);return results===null?"":decodeURIComponent(results[1].replace(/\+/g," "))}let PP=window.PP;const legitLvls=[PP.funnel.F_STATE_USER1,PP.funnel.F_STATE_USER2,PP.funnel.F_STATE_USER3];const qStringLvl=parseInt(getParameterByName("__uflvl"),10);if(legitLvls.includes(qStringLvl)){PP.funnel.current_state.userLvlOverride=qStringLvl}};window.PP.funnel.save_cookie=function(){let PP=window.PP;const newcookie=PP.funnel.state_encode(window.PP.funnel.current_state);PP.utils.log("new cookie:");PP.utils.log(newcookie);return PP.utils.cookie("ppfun",newcookie,120)};window.PP.funnel.article_version=function(){if(window.dataLayer&&window.dataLayer.articleVersion){if(window.dataLayer.articleVersion=="V5"){return"5"}else{return"3"}}else{return"3"}};window.PP.funnel.deactivate_modal=function(){window.PP.funnel.current_state.displayModal=false}; 114//# sourceMappingURL=user-funnel.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.