Page MenuHomeWickedGov Phorge

ext.centralNotice.bannerHistoryLogger.js
No OneTemporary

Size
9 KB
Referenced Files
None
Subscribers
None

ext.centralNotice.bannerHistoryLogger.js

/* eslint-disable no-unused-vars */
// TODO Remove above directive once inSample var is used.
/*
* Banner history logger mixin. Records an event every time this campaign is
* selected for the user (even if the banner is hidden). The log is kept in
* LocalStorage (via CentralNotice's kvStore). A sample of logs are sent to the
* server via EventLogging. Also allows forcing the log to be sent via
* cn.bannerHistoryLogger.ensureLogSent().
*/
( function () {
var cn = mw.centralNotice, // Guaranteed to exist; we depend on display RL module
bhLogger,
mixin = new cn.Mixin( 'bannerHistoryLogger' ),
doNotTrackEnabled =
// Support: Firefox < 32 (yes/no)
/1|yes/.test( navigator.doNotTrack ) ||
// Support: IE 11, Safari 7.1.3+ (window.doNotTrack)
window.doNotTrack === '1',
waitLogNoSendBeacon,
now = Math.round( Date.now() / 1000 ),
log,
readyToLogDeferredObj = $.Deferred(),
logSent = false,
alreadyRun = false,
inSample,
BANNER_HISTORY_KV_STORE_KEY = 'banner_history',
// Maximum time (in days) that the banner history store KV store item
// will persist if no entries are added to it.
BANNER_HISTORY_KV_STORE_TTL = 365,
BANNER_HISTORY_LOG_ENTRY_VERSION = 1, // Update when log format changes
EVENT_LOGGING_SCHEMA = 'CentralNoticeBannerHistory',
// The maximum random shift applied to timestamps, for user privacy
TIMESTAMP_RANDOM_SHIFT_MAX = 60;
/**
* Load the banner history log from KV storage
*/
function loadLog() {
log = cn.kvStore.getItem(
BANNER_HISTORY_KV_STORE_KEY,
cn.kvStore.contexts.GLOBAL
);
if ( !log ) {
log = [];
}
}
/**
* Return a log entry about the current campaign selection event.
*
* @return {Object}
*/
function makeLogEntry() {
var data = cn.data,
// Randomly shift timestamp +/- 0 to 10 seconds, so logs can't be
// linked to specific Web requests. This is to strengthen user
// privacy.
randomTimeShift =
Math.round( Math.random() * TIMESTAMP_RANDOM_SHIFT_MAX ) -
( TIMESTAMP_RANDOM_SHIFT_MAX / 2 ),
time = now + randomTimeShift,
logEntry = {
version: BANNER_HISTORY_LOG_ENTRY_VERSION,
language: data.uselang,
country: data.country,
isAnon: data.anonymous,
campaign: data.campaign,
campaignCategory: data.campaignCategory,
bucket: data.bucket,
time: time,
status: data.status,
statusCode: data.statusCode,
bannersNotGuaranteedToDisplay: !!data.bannersNotGuaranteedToDisplay
};
if ( data.banner ) {
logEntry.banner = data.banner;
}
if ( data.bannerCanceledReason ) {
logEntry.bannerCanceledReason = data.bannerCanceledReason;
}
if ( data.bannerLoadedButHiddenReason ) {
logEntry.bannerLoadedButHiddenReason = data.bannerLoadedButHiddenReason;
}
return logEntry;
}
/**
* Remove log entries older than maxEntryAge (in days) and, if necessary,
* remove entries to keep the total within maxEntries.
*
* @param {number} maxEntryAge
* @param {number} maxEntries
*/
function purgeOldLogEntries( maxEntryAge, maxEntries ) {
var i = 0,
cutoff = now - maxEntryAge * 86400;
// If we're above the max number of entries, pare it down, starting
// with older entries
if ( log.length > maxEntries ) {
log = log.slice( 0 - maxEntries );
}
// Remove any remaining entries that are older than maxEntryAge
while ( i < log.length && log[ i ].time < cutoff ) {
i++;
}
log = log.slice( i );
}
/**
* Store the contents of the log variable in kvStorage
*/
function storeLog() {
cn.kvStore.setItem(
BANNER_HISTORY_KV_STORE_KEY,
log,
cn.kvStore.contexts.GLOBAL,
BANNER_HISTORY_KV_STORE_TTL
);
}
/**
* Return an object with data for EventLogging.
*
* We scrunch the data as small as possible due to the WMF infrastructure's
* EventLogging payload limit.
*
* @param {number} rate The sampling rate used
* @return {Object}
*/
function makeEventLoggingData( rate ) {
var elData = {},
kvError = cn.kvStore.getError(),
i, logEntry, elLogEntry;
// Log ID: should be generated before this is called, and should not be
// persisted anywhere on the client (see below).
elData.i = bhLogger.id;
// sample rate
if ( rate ) {
elData.r = rate;
}
// if applicable, the message from any kv store error
if ( kvError ) {
elData.e = kvError.message;
return elData;
}
// total log length
elData.n = log.length;
elData.l = [];
// Add log entries, starting with the most recent ones, until the EL
// URL is too big, or we reach the end of the log.
i = log.length - 1;
while ( i >= 0 ) {
logEntry = log[ i ];
elLogEntry = [
logEntry.banner || '',
logEntry.campaign,
logEntry.time,
logEntry.statusCode
];
elData.l.unshift( elLogEntry.join( '|' ) );
if ( !checkEventLoggingURLSize( elData ) ) {
elData.l.shift();
break;
}
i--;
}
return elData;
}
/**
* Send the log via EventLogging.
*
* Returns a promise that resolves as soon as the log is submitted for users.
*
* @param {Object} elData Event log data
* @return {jQuery.Promise}
*/
function sendLog( elData ) {
var deferred = $.Deferred(),
elPromise;
elPromise = mw.eventLog.logEvent( EVENT_LOGGING_SCHEMA, elData );
elPromise.then( function () {
deferred.resolve();
}, function () {
deferred.reject();
} );
return deferred.promise();
}
/**
* Check the EventLogging URL we'd get from this data isn't too big. Here
* we copy some of the same processes done by ext.eventLogging.
*
* FIXME This is a temporary measure!
*
* @param elData
* @return {boolean} true if the EL payload size is OK
*/
function checkEventLoggingURLSize( elData ) {
return ( makeEventLoggingURL( elData ).length <= mw.eventLog.maxUrlSize );
}
/**
* Make an EventLogging URL ourselves.
* FIXME This is a temporary measure!
*
* @param elData
*/
function makeEventLoggingURL( elData ) {
return mw.eventLog.makeBeaconUrl( {
event: elData,
revision: 19079897, // Coordinate with extension.json
schema: EVENT_LOGGING_SCHEMA,
webHost: location.hostname,
wiki: mw.config.get( 'wgDBname' )
} );
}
// Set a function to run after the entire display process
mixin.setFinalizeChooseAndMaybeDisplayHandler( function ( mixinParams ) {
// Only run any processes once per pageview. This prevents multiple log
// entries per pageview if more than one attempted campaign has enabled
// this mixin. Note that there is currently no reconciliation for different
// banner history configuration settings in that scenario. See T261718.
if ( alreadyRun ) {
return;
}
alreadyRun = true;
waitLogNoSendBeacon = mixinParams.waitLogNoSendBeacon;
// Do this idly to avoid browser lock-ups
mw.requestIdleCallback( function () {
if ( !cn.kvStore.isAvailable() ) {
cn.kvStore.setNotAvailableError();
} else {
// If KV storage works here, do our stuff
loadLog();
// Only don't accumulate log entries if DNT is enabled... But do
// purge old entries.
if ( !doNotTrackEnabled ) {
log.push( makeLogEntry() );
}
purgeOldLogEntries(
mixinParams.maxEntryAge,
mixinParams.maxEntries
);
storeLog();
}
// Bow out now if DNT
if ( doNotTrackEnabled ) {
return;
}
// Load needed resources
// Note: we don't set the following up as RL dependencies because a
// lot of campaign filtering happens on the client, so many users
// see campaigns in choiceData that don't target them. If any of
// those campaigns were to use this mixin, all those users would
// needlessly get these dependencies. Also, they're not needed right
// away.
mw.loader.using( [
'mediawiki.util',
'mediawiki.user'
] ).done( function () {
// URL param bannerHistoryLogRate can override rate, for debugging
var rateParam = mw.util.getParamValue( 'bannerHistoryLogRate' ),
rate = rateParam !== null ?
parseFloat( rateParam ) : mixinParams.rate;
// We send back the temporary ID for all logs.
bhLogger.id = mw.user.generateRandomSessionId();
// Send a sample to the server
if ( Math.random() < rate ) {
sendLog( makeEventLoggingData( rate ) ).always( function () {
inSample = true;
logSent = true;
// By resolving only after sampling and possibly
// sending the log, we ensure that a sampled log
// would be sent first. That simplifies the logic
// for whether to send in other circumstances.
readyToLogDeferredObj.resolve();
} );
} else {
// If not in the sample, ready right away
readyToLogDeferredObj.resolve();
}
} );
} );
} );
// Register the mixin
cn.registerCampaignMixin( mixin );
// Object for public access
cn.bannerHistoryLogger = bhLogger = {
/**
* A client-generated unique ID for the log (on this pageview), not
* persisted in the log or anywhere else between pageviews.
*
* Note: this unique ID should not be stored anywhere on the client. It
* should be used only within the current browsing session to flag when
* a banner history is associated with a donation. If a user clicks on a
* banner to donate, it may be passed on to the WMF's donation sites via
* a URL parameter. Those sites should never store it on the client.
*/
id: null,
/**
* Send the banner history log to the server, if it wasn't sent already.
*
* @return {jQuery.Promise}
*/
ensureLogSent: function () {
var deferred = $.Deferred();
// Bow out if DNT
if ( doNotTrackEnabled ) {
deferred.resolve();
return deferred.promise();
}
// It's likely that this will be resolved by the time we get here
readyToLogDeferredObj.done( function () {
// This is included in the done() function to ensure a sampled
// log would be sent first (see above).
if ( logSent ) {
deferred.resolve();
} else {
sendLog( makeEventLoggingData() ).then( function () {
deferred.resolve();
}, function () {
deferred.reject();
} );
}
} );
return deferred.promise();
}
};
}() );

File Metadata

Mime Type
text/plain
Expires
Fri, Jul 3, 20:28 (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ac/2f/23169e93407682116e2f899b7145
Default Alt Text
ext.centralNotice.bannerHistoryLogger.js (9 KB)

Event Timeline