Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2751989
state.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
18 KB
Referenced Files
None
Subscribers
None
state.js
View Options
/**
* Stores data for campaign/banner processing and data related to the state of
* that processing. Provides cn.internal.state and cn.data.
*
* Note: Coordinate with CentralNoticeImpression schema; all data properties must
* either be in the schema and have the correct data type, or must be removed by
* calling getDataCopy( true ).
*/
(
function
()
{
var
state
,
status
,
campaignAttemptsManager
,
config
=
require
(
'./config.json'
),
impressionEventSampleRateOverridden
=
false
,
UNKNOWN_GEO_CODE
=
'XX'
,
// Campaign category, a temporary hack due to our hiccupy data model, is
// gleaned from the categories of all the banners in a campaign. In the
// unlikely case that a campaign contains banners of different
// categories, the campaign category is set to this value.
CAMPAIGN_CATEGORY_FOR_MIXED_BANNER_CATEGORIES
=
'mixed_banner_categories'
,
// These must coordinate with device names in the CN database
DEVICES
=
{
DESKTOP
:
'desktop'
,
IPHONE
:
'iphone'
,
IPAD
:
'ipad'
,
ANDROID
:
'android'
,
UNKNOWN
:
'unknown'
},
STATUSES
=
{
CAMPAIGN_NOT_CHOSEN
:
new
Status
(
'campaign_not_chosen'
,
0
),
// TODO Rename this status to ATTEMPTING_CAMPAIGN (T232236)
CAMPAIGN_CHOSEN
:
new
Status
(
'campaign_chosen'
,
1
),
// TODO Rename this status to CAMPAIGN_FAILED (T232236)
BANNER_CANCELED
:
new
Status
(
'banner_canceled'
,
2
),
NO_BANNER_AVAILABLE
:
new
Status
(
'no_banner_available'
,
3
),
BANNER_CHOSEN
:
new
Status
(
'banner_chosen'
,
4
),
BANNER_LOADED_BUT_HIDDEN
:
new
Status
(
'banner_loaded_but_hidden'
,
5
),
BANNER_SHOWN
:
new
Status
(
'banner_shown'
,
6
),
BANNER_LOADER_ERROR
:
new
Status
(
'banner_loader_error'
,
7
),
CHOICE_DATA_STALE
:
new
Status
(
'choice_data_stale'
,
8
)
},
// Until T114078 is closed, we minify banner history logs. This lookup
// table maps from hide reason string to a numeric code.
REASONS
=
{
// Any reason not listed here will be stored as "other".
other
:
0
,
close
:
1
,
waitdate
:
2
,
waitimps
:
3
,
waiterr
:
4
,
// Deprecated
belowMinEdits
:
5
,
viewLimit
:
6
,
'seen-fullscreen'
:
7
,
'cookies-disabled'
:
8
,
donate
:
9
,
cookies
:
10
,
seen
:
11
,
empty
:
12
,
waitnorestart
:
13
,
// Deprecated
waitnostorage
:
14
,
// TODO Switch impression diet to use just noStorage?
namespace
:
15
,
noStorage
:
16
,
requestedBannerNotAvailable
:
17
,
jsonParamError
:
18
,
bannerSequenceEmptyStep
:
19
,
bannerSequenceAllStepsSkipped
:
20
,
userOptOut
:
21
};
campaignAttemptsManager
=
(
function
()
{
var
attemptedCampaignStatusesByName
=
{},
hasOwn
=
Object
.
prototype
.
hasOwnProperty
;
return
{
setCampaignStatus
:
function
(
c
,
statusCode
)
{
var
statusObj
;
if
(
!
hasOwn
.
call
(
state
.
attemptedCampaignsByName
,
c
.
name
)
)
{
// If this is the first time we've seen this campaign, add it to the
// indexes and to the list of campiagn statuses.
statusObj
=
{
statusCode
:
statusCode
,
campaign
:
c
.
name
,
bannersCount
:
c
.
banners
.
length
};
state
.
data
.
campaignStatuses
.
push
(
statusObj
);
attemptedCampaignStatusesByName
[
c
.
name
]
=
statusObj
;
state
.
attemptedCampaignsByName
[
c
.
name
]
=
c
;
}
else
{
// Otherwise, just update the status code in campaign status object.
// The following will update the object in state.data.campaignStatuses,
// since the objects in that array are the same as the values of
// attemptedCampaignStatusesByName.
attemptedCampaignStatusesByName
[
c
.
name
].
statusCode
=
statusCode
;
}
},
getAttemptedCampaigns
:
function
()
{
return
state
.
data
.
campaignStatuses
.
map
(
function
(
statusObj
)
{
return
state
.
attemptedCampaignsByName
[
statusObj
.
campaign
];
}
);
}
};
}()
);
function
Status
(
key
,
code
)
{
this
.
key
=
key
;
this
.
code
=
code
;
}
/**
* Get a code for the general category the user's device is in.
*/
function
getDeviceCode
()
{
var
ua
;
// If we're on the desktop site, all your device are belong to DESKTOP
// TODO Fix this! Skin != device. Maybe screen width? P.S. Talk to users.
// TODO Make a test for this; it could stop working without notice.
// See also https://phabricator.wikimedia.org/T71366
if
(
mw
.
config
.
get
(
'skin'
)
!==
'minerva'
)
{
return
DEVICES
.
DESKTOP
;
}
ua
=
navigator
.
userAgent
;
if
(
/iphone/i
.
test
(
ua
)
)
{
return
DEVICES
.
IPHONE
;
}
if
(
/ipad/i
.
test
(
ua
)
)
{
return
DEVICES
.
IPAD
;
}
if
(
/android/i
.
test
(
ua
)
)
{
return
DEVICES
.
ANDROID
;
}
return
DEVICES
.
UNKNOWN
;
}
/**
* Set the data we need for campaign/banner selection and display, and for
* recording what happened. Here we load up all the data that's available
* initially.
*/
function
setInitialData
()
{
// Keep existing properties of state.urlParams, which may be set by tests
var
urlParams
=
Object
.
assign
(
state
.
urlParams
,
(
new
mw
.
Uri
()
).
query
),
impressionEventSampleRateFromUrl
;
state
.
data
.
anonymous
=
(
!
mw
.
user
.
isNamed
()
);
state
.
data
.
project
=
mw
.
config
.
get
(
'wgNoticeProject'
);
state
.
data
.
db
=
mw
.
config
.
get
(
'wgDBname'
);
state
.
data
.
optedOutCampaigns
=
getOptedOutCampaignsForUser
();
// All of the following may be overridden by URL parameters (including
// language, which can be overridden by uselang).
state
.
data
.
uselang
=
mw
.
config
.
get
(
'wgUserLanguage'
);
state
.
data
.
device
=
urlParams
.
device
||
getDeviceCode
();
// data.country already have been set
state
.
data
.
country
=
urlParams
.
country
||
state
.
data
.
country
||
UNKNOWN_GEO_CODE
;
// data.region should also already have been set, though it might be empty
state
.
data
.
region
=
urlParams
.
region
||
(
state
.
data
.
region
!==
undefined
?
state
.
data
.
region
:
false
)
||
UNKNOWN_GEO_CODE
;
// debug should be set no matter what
state
.
data
.
debug
=
(
urlParams
.
debug
!==
undefined
);
// The following four parameters should be used if they're numbers
state
.
data
.
randomcampaign
=
numericalUrlParamOrVal
(
urlParams
.
randomcampaign
,
Math
.
random
()
);
state
.
data
.
randombanner
=
numericalUrlParamOrVal
(
urlParams
.
randombanner
,
Math
.
random
()
);
state
.
data
.
recordImpressionSampleRate
=
numericalUrlParamOrVal
(
urlParams
.
recordImpressionSampleRate
,
mw
.
config
.
get
(
'wgCentralNoticeSampleRate'
)
);
// In the case of impressionEventSampleRate, also remember if it's overridden by
// a URL param
impressionEventSampleRateFromUrl
=
numericalUrlParamOrVal
(
urlParams
.
impressionEventSampleRate
,
null
);
if
(
impressionEventSampleRateFromUrl
!==
null
)
{
state
.
data
.
impressionEventSampleRate
=
impressionEventSampleRateFromUrl
;
impressionEventSampleRateOverridden
=
true
;
}
else
{
state
.
data
.
impressionEventSampleRate
=
mw
.
config
.
get
(
'wgCentralNoticeImpressionEventSampleRate'
);
}
// Legacy code exposed urlParams at mw.centralNotice.data.getVars.
// TODO Is this still needed? Maybe deprecate?
state
.
data
.
getVars
=
urlParams
;
// Contains list of campaigns statuses
state
.
data
.
campaignStatuses
=
[];
}
function
getOptedOutCampaignsForUser
()
{
var
allOptions
,
matches
,
key
,
blocked
=
[],
// Note: coordinate with CampaignType::PREFERENCE_KEY_PREFIX
regex
=
/^centralnotice-display-campaign-type-(.*)$/
;
if
(
mw
.
config
.
get
(
'wgUserName'
)
===
null
)
{
return
[];
}
allOptions
=
Object
.
assign
(
{},
mw
.
user
.
options
.
values
);
for
(
key
in
allOptions
)
{
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
allOptions
,
key
)
)
{
continue
;
}
matches
=
regex
.
exec
(
key
);
if
(
Array
.
isArray
(
matches
)
&&
matches
.
length
===
2
&&
!
allOptions
[
key
]
)
{
blocked
.
push
(
matches
[
1
]
);
}
}
return
blocked
;
}
function
numericalUrlParamOrVal
(
urlParam
,
val
)
{
var
urlParamAsFloat
=
parseFloat
(
urlParam
);
return
!
isNaN
(
urlParamAsFloat
)
?
urlParamAsFloat
:
val
;
}
function
setTestingBannerData
()
{
state
.
data
.
campaign
=
state
.
urlParams
.
campaign
;
state
.
data
.
banner
=
state
.
urlParams
.
banner
;
state
.
data
.
testingBanner
=
true
;
state
.
data
.
preview
=
(
state
.
urlParams
.
preview
!==
undefined
);
}
function
setStatus
(
s
,
reason
)
{
var
reasonCodeStr
=
reason
?
(
'.'
+
state
.
lookupReasonCode
(
reason
)
)
:
''
;
status
=
s
;
state
.
data
.
status
=
s
.
key
;
state
.
data
.
statusCode
=
s
.
code
.
toString
()
+
reasonCodeStr
;
// Update campaign status in the campaign attempts manager if a campaign is
// currently being attempted.
if
(
state
.
data
.
campaign
)
{
campaignAttemptsManager
.
setCampaignStatus
(
state
.
campaign
,
state
.
data
.
statusCode
);
}
}
/**
* State object (intended for access from within this RL module)
*/
state
=
mw
.
centralNotice
.
internal
.
state
=
{
STATUSES
:
STATUSES
,
// The following four properties are only exposed so QUnit
// tests can manipulate data
/**
* @private
*/
urlParams
:
{},
/**
* @private
*/
data
:
{},
/**
* @private
*/
campaign
:
null
,
/**
* @private
*/
banner
:
null
,
/**
* @private
*/
attemptedCampaignsByName
:
{},
/**
* Call this with geo data before calling setUp() or
* setUpForTestingBanner().
*
* @param geo
*/
setGeoData
:
function
(
geo
)
{
if
(
geo
)
{
state
.
data
.
country
=
geo
.
country
;
state
.
data
.
region
=
geo
.
region
;
}
},
/**
* Call this before calling setUp() or setUpForTestingBanner()
* if valid geo data is not available.
*/
setInvalidGeoData
:
function
()
{
state
.
data
.
country
=
UNKNOWN_GEO_CODE
;
state
.
data
.
region
=
UNKNOWN_GEO_CODE
;
},
setUp
:
function
()
{
setInitialData
();
setStatus
(
STATUSES
.
CAMPAIGN_NOT_CHOSEN
);
},
setUpForTestingBanner
:
function
()
{
setInitialData
();
// Load banner and campaign URL params into data
setTestingBannerData
();
// For testing, we'll set the status to what it normally is after
// a banner is chosen
setStatus
(
STATUSES
.
BANNER_CHOSEN
);
},
/**
* Return the data object, with data needed for campaign and banner
* selection, and data about the state of the selection process. The
* returned object should be considered read-only; i.e., don't modify
* it.
*/
getData
:
function
()
{
return
state
.
data
;
},
/**
* Get a copy of the data object. If cleanForLogging is true, remove
* properties or those that are not strings, numbers or booleans,
* to provide an object with properties appropriate to send as URL
* params (for legacy impression recordings) or as an impression event
* (via EventLogging).
*
* Note: Coordinate with CentralNoticeImpression schema; remove any
* data properties that do not conform to that schema.
*
* @param {boolean} prepareForLogging
*/
getDataCopy
:
function
(
prepareForLogging
)
{
var
dataCopy
=
$
.
extend
(
true
,
{},
state
.
data
);
if
(
prepareForLogging
)
{
delete
dataCopy
.
getVars
;
delete
dataCopy
.
mixins
;
delete
dataCopy
.
tests
;
delete
dataCopy
.
reducedBucket
;
delete
dataCopy
.
availableCampaigns
;
delete
dataCopy
.
optedOutCampaigns
;
// Serialized as JSON string for b/c, later, when we switch fully to EventLogging
// instead of the custom beacon/impression, the serialization could be removed
dataCopy
.
campaignStatuses
=
JSON
.
stringify
(
dataCopy
.
campaignStatuses
);
}
return
dataCopy
;
},
/**
* Set a list of campaigns that may be selected for this pageview. This method
* will be called to update the list on each iteration of the fallback loop.
*
* @param availableCampaigns
*/
setAvailableCampaigns
:
function
(
availableCampaigns
)
{
state
.
data
.
availableCampaigns
=
availableCampaigns
;
},
/**
* Sets the campaign that is currently being attempted. This campaign will be used
* by state as current, and if no others are attempted, final.
*
* @param {Object} c the campaign object, from the list of available campaigns
*/
setAttemptingCampaign
:
function
(
c
)
{
var
prop
,
i
,
category
,
campaignCategory
=
null
;
// Resetting previously set flags (if any)
delete
state
.
data
.
result
;
delete
state
.
data
.
reason
;
delete
state
.
data
.
bannerCanceledReason
;
delete
state
.
data
.
bannersNotGuaranteedToDisplay
;
state
.
campaign
=
c
;
state
.
data
.
campaign
=
c
.
name
;
// The following should ony be called _after_ state.campaign is set, otherwise
// the status won't be included in the record of attempted campaign statuses.
setStatus
(
STATUSES
.
CAMPAIGN_CHOSEN
);
// Provide the names of mixins enabled in this campaign. (By re-setting each time a
// campaign is attempted, we'll get here only mixins enabled for this specific campaign.)
// This is used in in-banner js to sanity-check that specific mixins are available and
// enabled.
state
.
data
.
mixins
=
{};
for
(
prop
in
c
.
mixins
)
{
if
(
Object
.
hasOwnProperty
.
call
(
c
.
mixins
,
prop
)
)
{
state
.
data
.
mixins
[
prop
]
=
true
;
}
}
// Set the campaignCategory property if all the banners in this
// campaign have the same category. This is necessary so we can
// use category even if a banner has not been chosen. In all normal
// cases, this won't be a problem.
// TODO Eventually, category should be a property of campaigns,
// not banners.
for
(
i
=
0
;
i
<
state
.
campaign
.
banners
.
length
;
i
++
)
{
category
=
state
.
campaign
.
banners
[
i
].
category
;
if
(
campaignCategory
===
null
)
{
campaignCategory
=
category
;
}
else
if
(
campaignCategory
!==
category
)
{
campaignCategory
=
CAMPAIGN_CATEGORY_FOR_MIXED_BANNER_CATEGORIES
;
break
;
}
}
state
.
data
.
campaignCategory
=
campaignCategory
;
// Is the campaign category among the categories configured to use
// legacy mechanisms?
state
.
data
.
campaignCategoryUsesLegacy
=
config
.
categoriesUsingLegacy
.
indexOf
(
campaignCategory
)
!==
-
1
;
},
/**
* Return the campaign currently being attempted, or null if no campaign has
* been attempted yet.
*/
getAttemptingCampaign
:
function
()
{
return
state
.
campaign
===
undefined
?
null
:
state
.
campaign
;
},
setBanner
:
function
(
b
)
{
state
.
banner
=
b
;
state
.
data
.
banner
=
state
.
banner
.
name
;
state
.
data
.
bannerCategory
=
state
.
banner
.
category
;
setStatus
(
STATUSES
.
BANNER_CHOSEN
);
},
setBucket
:
function
(
bucket
)
{
state
.
data
.
bucket
=
bucket
;
},
setReducedBucket
:
function
(
reducedBucket
)
{
state
.
data
.
reducedBucket
=
reducedBucket
;
},
setBannersNotGuaranteedToDisplay
:
function
()
{
state
.
data
.
bannersNotGuaranteedToDisplay
=
true
;
},
/**
* Legacy method, deprecated. Use failCampaign().
*
* @param {string} reason
*/
cancelBanner
:
function
(
reason
)
{
state
.
failCampaign
(
reason
);
},
/**
* Marks a campaign as failed.
*
* @param reason
*/
failCampaign
:
function
(
reason
)
{
state
.
data
.
bannerCanceledReason
=
reason
;
setStatus
(
STATUSES
.
BANNER_CANCELED
,
reason
);
// Legacy fields for Special:RecordImpression
state
.
data
.
result
=
'hide'
;
state
.
data
.
reason
=
reason
;
},
/**
* Legacy metod, deprecated. Use isCampaignFailed().
*/
isBannerCanceled
:
function
()
{
return
state
.
isCampaignFailed
();
},
isCampaignFailed
:
function
()
{
return
status
===
STATUSES
.
BANNER_CANCELED
;
},
isBannerShown
:
function
()
{
return
status
===
STATUSES
.
BANNER_SHOWN
;
},
setNoBannerAvailable
:
function
()
{
setStatus
(
STATUSES
.
NO_BANNER_AVAILABLE
);
// Legacy fields for Special:RecordImpression
state
.
data
.
result
=
'hide'
;
state
.
data
.
reason
=
'empty'
;
},
setRequestedBannerNotAvailable
:
function
(
bannerName
)
{
state
.
data
.
requestedBanner
=
bannerName
;
setStatus
(
STATUSES
.
NO_BANNER_AVAILABLE
,
'requestedBannerNotAvailable'
);
},
setBannerLoadedButHidden
:
function
(
reason
)
{
state
.
data
.
bannerLoadedButHiddenReason
=
reason
;
setStatus
(
STATUSES
.
BANNER_LOADED_BUT_HIDDEN
,
reason
);
// Legacy fields for Special:RecordImpression
state
.
data
.
result
=
'hide'
;
state
.
data
.
reason
=
reason
;
},
setAlterFunctionMissing
:
function
()
{
state
.
data
.
alterFunctionMissing
=
true
;
},
setBannerShown
:
function
()
{
setStatus
(
STATUSES
.
BANNER_SHOWN
);
// Legacy field for Special:RecordImpression
state
.
data
.
result
=
'show'
;
},
/**
* Sets banner_count, a legacy field for Special:RecordImpression
*
* @param bannerCount
*/
setBannerCount
:
function
(
bannerCount
)
{
// eslint-disable-next-line camelcase
state
.
data
.
banner_count
=
bannerCount
;
},
/**
* Sets minimal impression sample rate, the highest rate set will be used
*
* @param rate
*/
setMinRecordImpressionSampleRate
:
function
(
rate
)
{
// Update rate only if supplied rate is higher than current one
if
(
rate
>
state
.
data
.
recordImpressionSampleRate
)
{
state
.
data
.
recordImpressionSampleRate
=
rate
;
}
},
/**
* Sets minimal impression event sample rate, the highest rate set will be used
* (unless it was overridden by a URL parameter, in which that takes precedence).
*
* @param rate
*/
setMinImpressionEventSampleRate
:
function
(
rate
)
{
if
(
!
impressionEventSampleRateOverridden
&&
// Update rate only if supplied rate is higher than current one
rate
>
state
.
data
.
impressionEventSampleRate
)
{
state
.
data
.
impressionEventSampleRate
=
rate
;
}
},
/**
* Set a banner loader error, with an optional message
*
* @param {string} [msg]
*/
setBannerLoaderError
:
function
(
msg
)
{
if
(
msg
)
{
state
.
data
.
errorMsg
=
msg
;
}
setStatus
(
STATUSES
.
BANNER_LOADER_ERROR
);
},
/**
* Set a status indicating stale choice data was received.
*/
setChoiceDataStale
:
function
()
{
setStatus
(
STATUSES
.
CHOICE_DATA_STALE
);
},
/**
* Register that the current page view is included in a test.
*
* @param {string} identifier A string to identify the test. Should not contain
* commas.
*/
registerTest
:
function
(
identifier
)
{
var
tests
=
state
.
data
.
tests
=
state
.
data
.
tests
||
[];
// Add if it isn't already registered.
if
(
tests
.
indexOf
(
identifier
)
===
-
1
)
{
tests
.
push
(
identifier
);
if
(
tests
.
length
===
1
)
{
state
.
data
.
testIdentifiers
=
identifier
;
}
else
{
state
.
data
.
testIdentifiers
+=
','
+
identifier
;
}
}
},
/**
* Set a string with information for debugging. (All strings set here will be
* added to state data).
*
* @param {string} str A string with the debugging information; should not
* contain pipe characters ('|').
*/
setDebugInfo
:
function
(
str
)
{
if
(
!
state
.
data
.
debugInfo
)
{
state
.
data
.
debugInfo
=
str
;
}
else
{
state
.
data
.
debugInfo
+=
'|'
+
str
;
}
},
lookupReasonCode
:
function
(
reasonName
)
{
if
(
reasonName
in
REASONS
)
{
return
REASONS
[
reasonName
];
}
return
REASONS
.
other
;
},
/**
* Returns number of campaigns were chosen
*
* @return {number}
*/
countCampaignsAttempted
:
function
()
{
return
state
.
data
.
campaignStatuses
.
length
;
},
getAttemptedCampaigns
:
function
()
{
return
campaignAttemptsManager
.
getAttemptedCampaigns
();
}
};
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Jul 3, 19:25 (1 d, 11 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
2f/e6/74853a805fb5c2276a5034866350
Default Alt Text
state.js (18 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment