| Current File : /home/jvzmxxx/wiki1/extensions/EventLogging/modules/ext.eventLogging.core.js |
/*!
* @module ext.eventLogging.core
* @author Ori Livneh <ori@wikimedia.org>
*/
( function ( mw, $ ) {
'use strict';
var self,
// `baseUrl` corresponds to $wgEventLoggingBaseUri, as declared
// in EventLogging.php. If the default value of 'false' has not
// been overridden, events will not be sent to the server.
baseUrl = mw.config.get( 'wgEventLoggingBaseUri' );
/**
* Client-side EventLogging API.
*
* The main API is `mw.eventLog.logEvent`. Most other methods represent internal
* functionality, which is exposed only to ease debugging code and writing tests.
*
* Instances of `ResourceLoaderSchemaModule` indicate a dependency on this module and
* declare themselves via the declareSchema method.
*
* Developers should not load this module directly, but work with schema modules instead.
* Schema modules will load this module as a dependency.
*
* @private
* @class mw.eventLog.Core
* @singleton
*/
self = {
/**
* Schema registry. Schemas that have been declared explicitly via
* `eventLog.declareSchema` or implicitly by being referenced in an
* `eventLog.logEvent` call are stored in this object.
*
* @private
* @property schemas
* @type Object
*/
schemas: {},
/**
* Maximum length in chars that a beacon URL can have.
* Relevant:
*
* - Length that browsers support (http://stackoverflow.com/a/417184/319266)
* - Length that proxies support (e.g. Varnish)
* - varnishlog (shm_reclen)
* - varnishkafka
*
* @private
* @property maxUrlSize
* @type Number
*/
maxUrlSize: 2000,
/**
* Load a schema from the schema registry.
* If the schema does not exist, it will be initialised.
*
* @private
* @param {string} schemaName Name of schema.
* @return {Object} Schema object.
*/
getSchema: function ( schemaName ) {
if ( !self.schemas.hasOwnProperty( schemaName ) ) {
self.schemas[ schemaName ] = { schema: { title: schemaName } };
}
return self.schemas[ schemaName ];
},
/**
* Register a schema so that it can be used to validate events.
* `ResourceLoaderSchemaModule` instances generate JavaScript code that
* invokes this method.
*
* @private
* @param {string} schemaName Name of schema.
* @param {Object} meta An object describing a schema:
* @param {number} meta.revision Revision ID.
* @param {Object} meta.schema The schema itself.
* @return {Object} The registered schema.
*/
declareSchema: function ( schemaName, meta ) {
return $.extend( true, self.getSchema( schemaName ), meta );
},
/**
* Checks whether a JavaScript value conforms to a specified
* JSON Schema type.
*
* @private
* @param {Object} value Object to test.
* @param {string} type JSON Schema type.
* @return {boolean} Whether value is instance of type.
*/
isInstanceOf: function ( value, type ) {
var jsType = $.type( value );
switch ( type ) {
case 'integer':
return jsType === 'number' && value % 1 === 0;
case 'number':
return jsType === 'number' && isFinite( value );
case 'timestamp':
return jsType === 'date' || ( jsType === 'number' && value >= 0 && value % 1 === 0 );
default:
return jsType === type;
}
},
/**
* Check whether a JavaScript object conforms to a JSON Schema.
*
* @private
* @param {Object} obj Object to validate.
* @param {Object} schema JSON Schema object.
* @return {Array} An array of validation errors (empty if valid).
*/
validate: function ( obj, schema ) {
var key, val, prop,
errors = [];
if ( !schema || !schema.properties ) {
errors.push( 'Missing or empty schema' );
return errors;
}
for ( key in obj ) {
if ( !schema.properties.hasOwnProperty( key ) ) {
errors.push( mw.format( 'Undeclared property "$1"', key ) );
}
}
for ( key in schema.properties ) {
prop = schema.properties[ key ];
if ( !obj.hasOwnProperty( key ) ) {
if ( prop.required ) {
errors.push( mw.format( 'Missing property "$1"', key ) );
}
continue;
}
val = obj[ key ];
if ( !( self.isInstanceOf( val, prop.type ) ) ) {
errors.push( mw.format(
'Value $1 is the wrong type for property "$2" ($3 expected)',
JSON.stringify( val ), key, prop.type
) );
continue;
}
if ( prop[ 'enum' ] && $.inArray( val, prop[ 'enum' ] ) === -1 ) {
errors.push( mw.format(
'Value $1 for property "$2" is not one of $3',
JSON.stringify( val ), key, JSON.stringify( prop[ 'enum' ] )
) );
}
}
return errors;
},
/**
* Sets default property values for events belonging to a particular schema.
* If default values have already been declared, the new defaults are merged
* on top.
*
* @param {string} schemaName The name of the schema.
* @param {Object} schemaDefaults A map of property names to default values.
* @return {Object} Combined defaults for schema.
*/
setDefaults: function ( schemaName, schemaDefaults ) {
return self.declareSchema( schemaName, { defaults: schemaDefaults } );
},
/**
* Prepares an event for dispatch by filling defaults for any missing
* properties and by encapsulating the event object in an object which
* contains metadata about the event itself.
*
* @private
* @param {string} schemaName Canonical schema name.
* @param {Object} eventData Event instance.
* @return {Object} Encapsulated event.
*/
prepare: function ( schemaName, eventData ) {
var schema = self.getSchema( schemaName ),
event = $.extend( true, {}, schema.defaults, eventData ),
errors = self.validate( event, schema.schema );
while ( errors.length ) {
mw.track( 'eventlogging.error', mw.format( '[$1] $2', schemaName, errors.pop() ) );
}
return {
event: event,
revision: schema.revision || -1,
schema: schemaName,
webHost: location.hostname,
wiki: mw.config.get( 'wgDBname' )
};
},
/**
* Constructs the EventLogging URI based on the base URI and the
* encoded and stringified data.
*
* @private
* @param {Object} data Payload to send
* @return {string|boolean} The URI to log the event.
*/
makeBeaconUrl: function ( data ) {
var queryString = encodeURIComponent( JSON.stringify( data ) );
return baseUrl + '?' + queryString + ';';
},
/**
* Checks whether a beacon url is short enough,
* so that it does not get truncated by varnishncsa.
*
* @private
* @param {string} schemaName Canonical schema name.
* @param {string} url Beacon url.
* @return {string|undefined} The error message in case of error.
*/
checkUrlSize: function ( schemaName, url ) {
var message;
if ( url.length > self.maxUrlSize ) {
message = 'Url exceeds maximum length';
mw.eventLog.logFailure( schemaName, 'urlSize' );
mw.track( 'eventlogging.error', mw.format( '[$1] $2', schemaName, message ) );
return message;
}
},
/**
* Transfer data to a remote server by making a lightweight HTTP
* request to the specified URL.
*
* If the user expressed a preference not to be tracked, or if
* $wgEventLoggingBaseUri is unset, this method is a no-op.
*
* @param {string} url URL to request from the server.
* @return undefined
*/
sendBeacon: ( /1|yes/.test( navigator.doNotTrack ) || !baseUrl )
? $.noop
: navigator.sendBeacon
? function ( url ) { try { navigator.sendBeacon( url ); } catch ( e ) {} }
: function ( url ) { document.createElement( 'img' ).src = url; },
/**
* Construct and transmit to a remote server a record of some event
* having occurred. Events are represented as JavaScript objects that
* conform to a JSON Schema. The schema describes the properties the
* event object may (or must) contain and their type. This method
* represents the public client-side API of EventLogging.
*
* @param {string} schemaName Canonical schema name.
* @param {Object} eventData Event object.
* @return {jQuery.Promise} jQuery Promise object for the logging call.
*/
logEvent: function ( schemaName, eventData ) {
var event = self.prepare( schemaName, eventData ),
url = self.makeBeaconUrl( event ),
sizeError = self.checkUrlSize( schemaName, url ),
deferred = $.Deferred();
if ( !sizeError ) {
self.sendBeacon( url );
deferred.resolveWith( event, [ event ] );
} else {
deferred.rejectWith( event, [ event, sizeError ] );
}
return deferred.promise();
},
/**
* Increment the error count in statsd for this schema.
*
* Should be called instead of logEvent in case of an error.
*
* @param {string} schemaName
* @param {string} errorCode
*/
logFailure: function ( schemaName, errorCode ) {
// Record this failure as a simple counter. By default "counter.*" goes nowhere.
// The WikimediaEvents extension sends it to statsd.
mw.track( 'counter.eventlogging.client_errors.' + schemaName + '.' + errorCode );
}
};
// Output validation errors to the browser console, if available.
mw.trackSubscribe( 'eventlogging.error', function ( topic, error ) {
mw.log.error( error );
} );
/**
* @class mw.eventLog
* @mixins mw.eventLog.Core
*/
$.extend( mw.eventLog, self );
}( mediaWiki, jQuery ) );