PageSourceSearch

https://assets.propublica.org/static/prod/v5/js/deploy/user-funnel.js

js propublica.org collected 2026-09-24 07:35:13 UTC 14,661 bytes, 114 lines download raw bytes

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.