Skip to content

Commit

Permalink
Added javascript logging (initial attempt, please inform me of any is…
Browse files Browse the repository at this point in the history
…sues).

Updated README.md to represent changes
  • Loading branch information
Blizzke committed May 22, 2015
1 parent c08362d commit 37eeef9
Show file tree
Hide file tree
Showing 23 changed files with 464 additions and 265 deletions.
58 changes: 32 additions & 26 deletions Auditing.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* @author Steve Guns <steve@bedezign.com>
* @package com.bedezign.yii2.audit
* @copyright 2014 B&E DeZign
* @copyright 2014-2015 B&E DeZign
*/

namespace bedezign\yii2\audit;
Expand Down Expand Up @@ -38,36 +38,36 @@
*/
class Auditing extends \yii\base\Module
{
/** @var string name of the component to use for database access */
public $db = 'db';
/** @var string name of the component to use for database access */
public $db = 'db';

/** @var string[] List of actions to track. '*' is allowed as the last character to use as wildcard. */
public $trackActions = ['*'];
/** @var string[] List of actions to track. '*' is allowed as the last character to use as wildcard. */
public $trackActions = ['*'];

/** @var string[] Actions to ignore. '*' is allowed as the last character to use as wildcard (eg 'debug/*'). */
public $ignoreActions = [];
/** @var string[] Actions to ignore. '*' is allowed as the last character to use as wildcard (eg 'debug/*'). */
public $ignoreActions = [];

/** @var int Chance in % that the truncate operation will run, false to not run at all */
public $truncateChance = false;
/** @var int Chance in % that the truncate operation will run, false to not run at all */
public $truncateChance = false;

/** @var int Maximum age (in days) of the audit entries before they are truncated */
public $maxAge = null;
/** @var int Maximum age (in days) of the audit entries before they are truncated */
public $maxAge = null;

/** @var int[] (List of) user(s) IDs with access to the viewer, null for everyone (if the role matches) */
public $accessUsers = null;
/** @var int[] (List of) user(s) IDs with access to the viewer, null for everyone (if the role matches) */
public $accessUsers = null;

/** @var string[] (List of) role(s) with access to the viewer, null for everyone (if the user matches) */
public $accessRoles = 'admin';
/** @var string[] (List of) role(s) with access to the viewer, null for everyone (if the user matches) */
public $accessRoles = 'admin';

/** @var static The current instance */
private static $current = null;
/** @var static The current instance */
private static $_current = null;

/** @var audit\models\AuditEntry If activated this is the active entry*/
private $entry = null;
private $_entry = null;

public function init()
{
static::$current = $this;
static::$_current = $this;

parent::init();

Expand All @@ -87,6 +87,10 @@ public function init()
\Yii::$app->on(Application::EVENT_AFTER_REQUEST, [$this, 'onAfterRequest']);
}

/**
* Called to evaluate if the current request should be logged
* @param \yii\base\Event $event
*/
public function onApplicationAction($event)
{
$actionId = $event->action->uniqueId;
Expand All @@ -107,7 +111,7 @@ public function onApplicationAction($event)
public function onAfterRequest()
{
if ($this->entry)
$this->entry->finalize();
$this->_entry->finalize();

if ($this->truncateChance !== false && $this->maxAuditAge !== null) {
if (rand(1, 100) <= $this->truncateChance)
Expand Down Expand Up @@ -140,22 +144,24 @@ public function getDb()
}

/**
* Returns the current module instance.
* Since we don't know how the module was linked into the application, this function allows us to retrieve
* the instance without that information. As soon as an instance was initialized, it is linked.
* @return static
*/
public static function current()
{
return static::$current;
return static::$_current;
}

public function getEntry($create = false)
{
if ($create && !$this->entry) {

$this->entry = models\AuditEntry::create(true);
$this->entry->save(false);
if (!$this->_entry && $create) {
$this->_entry = models\AuditEntry::create(true);
$this->_entry->save(false);
}

return $this->entry;
return $this->_entry;
}

/**
Expand Down
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ This is based on a couple of other projects out there:
## Features
Installs as a simple module so it can be added without too much hassle.

Once added, this module tracks all incoming pageviews with the ability to add custom data to a view.
* Tracks all incoming pageviews with the ability to add custom data to a view.
It logs the user-id (if any), IP, superglobals ($_GET/$_POST/$_SERVER/$_FILES/$_COOKIES), memory usage, referrer and origin. You can either track specific actions and nothing else or exclude specific routes from logging (wildcard supported).

It logs the user-id (if any), IP, superglobals ($_GET/$_POST/$_SERVER/$_FILES/$_COOKIES), memory usage, referrer and origin.
* Track database changes. By implementing the `AuditingBehavior` this is easily realized thanks to a modified version of [Sammayes Yii2 Audit Trail](https://github.com/Sammaye/yii2-audittrail).

Additionally it can save database changes thanks to a modified version of [Sammayes Yii2 Audit Trail](https://github.com/Sammaye/yii2-audittrail).
* Automatically log javascript errors. Errors and warning are logged automatically (if you activate the functionality), but the javascript component also provides methods to manually add logging entries.

* View your data. The module contains a nice viewer that is automatically made available when you add it to your configuration. It has configurable permissions to limit access to this functionality.

## Installing

Expand Down Expand Up @@ -53,7 +56,7 @@ This installs the module with auto loading, instructing it to not log anything d

### Error Logging

If you also want errors to be logged, you have to register the included errorhandler as well in you configuration:
If you want errors to be logged, you have to register the included errorhandler as well in you configuration:

'errorHandler' => [
'class' => '\bedezign\yii2\audit\components\web\ErrorHandler',
Expand Down Expand Up @@ -86,16 +89,43 @@ If you want database changes to be logged, you have to add the `AuditingBehavior
]
];
}


### Javascript Logging

## Usage
The module also supports logging of javascript errors, warnings and even regular log entries.
To activate, register the `assets\JSLoggingAsset` in any of your views:

You can then access the auditing module through the following URL:
\bedezign\yii2\audit\assets\JSLoggingAsset::register($this);
This will activate the logger automatically. By default all warnings and errors are transmitted to the backend.

http://localhost/path/to/index.php?r=auditing
The default configuration assumes that the module was added as "auditing" (so the log url would be "*/auditing/javascript/log*"). If that is not the case, please make sure to update the setting somewhere in your javascript:

window.jsLogger.logUrl = '/mymodulename/javascript/log';

All javascript logging will be linked to the entry associated with the page entry created when you performed the initial request. This is accomplished by adding the ID of that entry in the `window`-object (`window.auditEntry`).

You can add extra data to an entry by calling:
Beware: If you use ajax or related technologies to load data from the backend, these requests might generate their own auditing entries. If those cause backend errors they will be linked to that new entry. This might be a bit weird with the javascript logging being linked to the older entry.

### Extra Data

It is possible to add extra custom data to the current audit entry by simply calling:

\bedezign\yii2\audit\Auditing::current()->data('name', 'extra data can be an integer, string, array, object or whatever', 'optional type');

Or if you prefer:

\Yii::$app->auditing->data(('name', 'extra data can be an integer, string, array, object or whatever', 'optional type');


## Viewing the audit data

Assuming you named the module "auditing" you can then access the auditing module through the following URL:

http://localhost/path/to/index.php?r=auditing



### Screenshots

Expand Down
35 changes: 35 additions & 0 deletions assets/JSLoggingAsset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* AssetBundle to register when you want to log javascript events as well.
*/

namespace bedezign\yii2\audit\assets;

class JSLoggingAsset extends \yii\web\AssetBundle
{
public $sourcePath = '@bedezign/yii2/audit/assets';

public $js = [
'javascript/logger.js',
];

public function init()
{
// Activate the logging as soon as we can
$this->jsOptions['position'] = \yii\web\View::POS_HEAD;
parent::init();
}

public function publish($assetManager)
{
$module = \bedezign\yii2\audit\Auditing::current();
if ($module && $module->entry)
// We can't be sure that the actual logger was loaded already, so we fallback on the window object
// to store the associated audit entry id
\Yii::$app->view->registerJs("window.auditEntry = {$module->entry->id};", \yii\web\View::POS_HEAD);

return parent::publish($assetManager);
}


}
8 changes: 2 additions & 6 deletions assets/AuditingAsset.php → assets/ViewerAsset.php
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
<?php
/**
* Auditing assets
*
* @author Steve Guns <steve@bedezign.com>
*/

namespace bedezign\yii2\audit\assets;

use yii\web\AssetBundle;

class AuditingAsset extends AssetBundle
class ViewerAsset extends AssetBundle
{
/**
* @inheritdoc
Expand All @@ -22,6 +17,7 @@ class AuditingAsset extends AssetBundle
public $css = [
'css/auditing.css',
];

public $depends = [
'yii\bootstrap\BootstrapAsset',
];
Expand Down
84 changes: 84 additions & 0 deletions assets/javascript/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
(function (window, document) {
if (!window.jsLogger) {
window.jsLogger = new function() {
// The url to post captured log entries to (default assumes the auditing module was added as "auditing").
this.logUrl = '/auditing/javascript/log';

// The types that should be sent to the backend.
this.captureTypes = ['warn', 'error', 'onerror'];

// True if you also want any error to be forwarded to the appropriate console function
this.consoleOutput = typeof window.console !== 'undefined' && typeof window.console.log !== 'undefined';

// True to pass on the error to the previously active handler
this.chainErrors = true;

var previousErrorHandler, errorHandler = function(message, file, line, col, error) {
// Also send the the error object
window.jsLogger.capture('onerror', message, {error: error}, file, line, col);
if (typeof previousErrorHandler == 'function' && this.chainErrors)
return previousErrorHandler(message, file, line, col, error);
};

this.info = function (message, data) {
if (this.consoleOutput) console.info.apply(console, arguments);
this.capture('info', message, data);
};

this.log = function(message, data) {
if (this.consoleOutput) console.log.apply(console, arguments);
this.capture('log', message, data);
};

this.warn = function (message, data) {
if (this.consoleOutput) console.warn.apply(console, arguments);
this.capture('warn', message, data);
};

this.error = function (message, data) {
if (this.consoleOutput) console.error.apply(console, arguments);
this.capture('error', message, data);
};

this.attachErrorHandler = function() {
if (typeof previousErrorHandler != 'function' || previousErrorHandler != errorHandler)
previousErrorHandler = window.onerror
window.onerror = errorHandler;
};

this.attachDojoErrorHandler = function() {
/*dojo.connect(console, "error", function(error, trace) {
window.jsLogger.capture('onerror', error.message, {error: error, trace: trace}, error.fileName, error.lineNumber, error.col);
});*/
}

this.capture = function(type, message, data, file, line, col) {
if (window.XMLHttpRequest && this.captureTypes.indexOf(type.toLowerCase()) != -1) {
var xhr = new XMLHttpRequest(),
log = {type: type, message: message, data: data, file: file, line: line, col: col};
if (window.auditEntry)
log.auditEntry = window.auditEntry;

xhr.open('POST', this.logUrl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.send('data=' + encodeURIComponent(JSON.stringify(log)));
}
return true;
};

// Wrap console functions in a native function to make sure they have an "apply" function.
// IE9 console functions are objects and don't have it (http://stackoverflow.com/a/5539378/50158)
if (Function.prototype.bind && window.console && typeof console.log == "object") {
["log","info","warn","error","assert","dir","clear","profile","profileEnd"]
.forEach(function (method) { console[method] = this.bind(console[method], console); }, Function.prototype.call);
}

// Allow for stringify'ing Error objects (http://stackoverflow.com/a/18391400/50158)
Object.defineProperty(Error.prototype, 'toJSON', { value: function () {
var alt = {}; Object.getOwnPropertyNames(this).forEach(function (key) { alt[key] = this[key]; }, this); return alt;
}, configurable: true });

this.attachErrorHandler();
};
}
})(window, document);
Loading

0 comments on commit 37eeef9

Please sign in to comment.