Page MenuHomeWickedGov Phorge

state.js
No OneTemporary

Size
18 KB
Referenced Files
None
Subscribers
None

state.js

/**
* 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

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)

Event Timeline