Skip to content

Commit

Permalink
Add Click model for advanced analytics, add Setup settings, use Chart…
Browse files Browse the repository at this point in the history
….js for charting, create stat pages and routes
  • Loading branch information
cydrobolt committed Dec 28, 2016
1 parent a2d4b84 commit b8a1c7d
Show file tree
Hide file tree
Showing 18 changed files with 508 additions and 13 deletions.
40 changes: 40 additions & 0 deletions app/Helpers/ClickHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
namespace App\Helpers;
use App\Models\Click;
use App\Models\Link;
use Illuminate\Http\Request;

class ClickHelper {
static private function getCountry($ip) {
// TODO
return 'US';
}

static private function getHost($url) {
// Return host given URL; NULL if host is
// not found.
return parse_url($url, PHP_URL_HOST);
}

static public function recordClick(Link $link, Request $request) {
/**
* Given a Link model instance and Request object, process post click operations.
* @param Link model instance $link
* @return boolean
*/

$ip = $request->ip();
$referer = $request->server('HTTP_REFERER');

$click = new Click;
$click->link_id = $link->id;
$click->ip = $ip;
$click->country = self::getCountry($ip);
$click->referer = $referer;
$click->referer_host = ClickHelper::getHost($referer);
$click->user_agent = $request->server('HTTP_USER_AGENT');
$click->save();

return true;
}
}
8 changes: 0 additions & 8 deletions app/Helpers/LinkHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ static public function validateEnding($link_ending) {
return $is_valid_ending;
}

static public function processPostClick($link) {
/**
* Given a Link model instance, process post click operations.
* @param Link model instance $link
* @return boolean
*/
}

static public function findPseudoRandomEnding() {
/**
* Return an available pseudorandom string of length _PSEUDO_RANDOM_KEY_LENGTH,
Expand Down
6 changes: 5 additions & 1 deletion app/Http/Controllers/LinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Factories\LinkFactory;
use App\Helpers\CryptoHelper;
use App\Helpers\LinkHelper;
use App\Helpers\ClickHelper;

class LinkController extends Controller {
/**
Expand Down Expand Up @@ -93,8 +94,11 @@ public function performRedirect(Request $request, $short_url, $secret_key=false)
$link->clicks = $clicks;
$link->save();

if (env('SETTING_ADV_ANALYTICS')) {
// Record advanced analytics if option is enabled
ClickHelper::recordClick($link, $request);
}
// Redirect to final destination
LinkHelper::processPostClick($link);
return redirect()->to($long_url, 301);
}

Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/SetupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public static function performSetup(Request $request) {
$st_auto_api_key = $request->input('setting:auto_api_key');
$st_anon_api = $request->input('setting:anon_api');
$st_pseudor_ending = $request->input('setting:pseudor_ending');
$st_adv_analytics = $request->input('setting:adv_analytics');

$mail_host = $request->input('app:smtp_server');
$mail_port = $request->input('app:smtp_port');
Expand Down Expand Up @@ -160,6 +161,7 @@ public static function performSetup(Request $request) {
'ST_AUTO_API' => $st_auto_api_key,
'ST_ANON_API' => $st_anon_api,
'ST_PSEUDOR_ENDING' => $st_pseudor_ending,
'ST_ADV_ANALYTICS' => $st_adv_analytics,

'TMP_SETUP_AUTH_KEY' => $setup_auth_key
])->render();
Expand Down
76 changes: 76 additions & 0 deletions app/Http/Controllers/StatsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Redirect;

use App\Models\Link;
use App\Models\Clicks;
use Illuminate\Support\Facades\DB;

class StatsController extends Controller {
private function getDayStats($link_id) {
// date => x
// clicks => y
$stats = DB::table('clicks')
->select(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d') as x, count(*) as y"))
->where('link_id', $link_id)
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d')"))
->orderBy('x', 'asc')
->get();

return $stats;
}

private function getCountryStats($link_id) {
$stats = DB::table('clicks')
->select(DB::raw("country as label, count(*) as clicks"))
->where('link_id', $link_id)
->groupBy('country')
->orderBy('clicks', 'desc')
->get();

return $stats;
}

private function getRefererStats($link_id) {
$stats = DB::table('clicks')
->select(DB::raw("COALESCE(referer_host, 'Direct') as label, count(*) as clicks"))
->where('link_id', $link_id)
->groupBy('referer_host')
->orderBy('clicks', 'desc')
->get();

return $stats;
}


public function displayStats(Request $request, $short_url) {
if (!$this->isLoggedIn()) {
return redirect(route('login'))->with('error', 'Please login to view link stats.');
}

$link = Link::where('short_url', $short_url)
->first();
$link_id = $link->id;

// Return 404 if link not found
if ($link == null) {
return redirect(route('admin'))->with('error', 'Cannot show stats for nonexistent link.');
}

if ( (session('username') != $link->creator) && !self::currIsAdmin() ) {
return redirect(route('admin'))->with('error', 'You do not have permission to view stats for this link.');
}

$day_stats = $this->getDayStats($link_id);
$country_stats = $this->getCountryStats($link_id);
$referer_stats = $this->getRefererStats($link_id);

return view('link_stats', [
'link' => $link,
'day_stats' => $day_stats,
'country_stats' => $country_stats,
'referer_stats' => $referer_stats
]);
}
}
1 change: 1 addition & 0 deletions app/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
$app->get('/{short_url}', ['uses' => 'LinkController@performRedirect']);
$app->get('/{short_url}/{secret_key}', ['uses' => 'LinkController@performRedirect']);

$app->get('/admin/stats/{short_url}', ['uses' => 'StatsController@displayStats']);

/* POST endpoints */

Expand Down
7 changes: 7 additions & 0 deletions app/Models/Click.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Click extends Model {
protected $table = 'clicks';
}
1 change: 0 additions & 1 deletion app/Models/Link.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Link extends Model {
Expand Down
1 change: 0 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@

class User extends Model {
protected $table = 'users';

}
44 changes: 44 additions & 0 deletions database/migrations/2016_12_27_232934_create_clicks_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateClicksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('clicks', function(Blueprint $table)
{
$table->engine = 'InnoDB';

$table->increments('id');
$table->string('ip');
$table->string('country')->nullable();
$table->string('referer')->nullable();
$table->string('referer_host')->nullable();
$table->text('user_agent')->nullable();

$table->index('ip');
$table->index('referer');
$table->integer('link_id')->unsigned();
$table->foreign('link_id')->references('id')->on('links')->onDelete('cascade');

$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('clicks');
}
}
3 changes: 3 additions & 0 deletions public/css/stats.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.stats-header {
text-align: center;
}
84 changes: 84 additions & 0 deletions public/js/StatsCtrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
polr.controller('StatsCtrl', function($scope, $compile) {
$scope.dayChart = null;
$scope.refererChart = null;
$scope.countryChart = null;

$scope.dayData = dayData;
$scope.refererData = refererData;
$scope.countryData = countryData;

$scope.initDayChart = function () {
var ctx = $("#dayChart");
$scope.dayChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Clicks',
data: $scope.dayData
}]
},
options: {
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'day'
}
}],
yAxes: [{
ticks: {
min: 0
}
}]
}
}
});
};
$scope.initRefererChart = function () {
// Traffic sources
var ctx = $("#refererChart");

var srcLabels = [];
// var bgColors = [];
var bgColors = [ '#003559', '#162955', '#2E4272', '#4F628E', '#7887AB', '#b9d6f2'];
var srcData = [];

_.each($scope.refererData, function (item) {
if (srcLabels.length > 6) {
// If more than 6 referers are listed, push the seventh and
// beyond into "other"
srcLabels[6] = 'Other';
srcData[6] += item.clicks;
bgColors[6] = 'brown';
return;
}

srcLabels.push(item.label);
srcData.push(item.clicks);
});

$scope.refererChart = new Chart(ctx, {
type: 'pie',
data: {
labels: srcLabels,
datasets: [{
data: srcData,
backgroundColor: bgColors
}]
}
});

$('#refererTable').DataTable();
};
$scope.initCountryChart = function () {

};
$scope.init = function () {
$scope.initDayChart();
$scope.initRefererChart();
$scope.initCountryChart();
};

$scope.init();

});
16 changes: 16 additions & 0 deletions public/js/chart.bundle.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit b8a1c7d

Please sign in to comment.