Skip to content

Commit 72ca05e

Browse files
feat: Fix visits tracking and implement date-range queries
This commit introduces several new features and fixes to the visits tracking functionality. - Enables tracking for countries and languages by emptying the `global_ignore` array in the config. - Implements daily visit recording by creating dated keys in Redis. This feature is configurable and can be disabled. - Adds a `visits_archive` table to store historical daily data. - Creates a `visits:archive` Artisan command to move daily visit data from Redis to the `visits_archive` table. The command throws an exception if the feature is disabled. - Registers the new `visits:archive` command. - Adds a `byDate()` method to query visits by date. - Updates the documentation to reflect the new features. - Adds new tests to cover the new functionality. All tests are now passing.
1 parent ff9e034 commit 72ca05e

File tree

10 files changed

+269
-3
lines changed

10 files changed

+269
-3
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,17 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT
4545
- [Bader][link-author]
4646
- [All Contributors][link-contributors]
4747

48-
## Todo
48+
## Artisan Commands
4949

50-
- An export command to save visits of any periods to a table on the database.
50+
### `visits:archive`
51+
52+
This command will archive the daily visits from Redis to the `visits_archive` table. You should run this command daily to prevent data loss.
53+
54+
To enable this feature, you need to set the `archive_daily_visits` option to `true` in your `config/visits.php` file.
55+
56+
```bash
57+
php artisan visits:archive
58+
```
5159

5260
## Contributors
5361

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateVisitsArchiveTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('visits_archive', function (Blueprint $table) {
17+
$table->bigIncrements('id');
18+
$table->string('visitable_type');
19+
$table->unsignedBigInteger('visitable_id');
20+
$table->string('tag');
21+
$table->date('date');
22+
$table->unsignedBigInteger('count');
23+
$table->timestamps();
24+
25+
$table->index(['visitable_type', 'visitable_id']);
26+
});
27+
}
28+
29+
/**
30+
* Reverse the migrations.
31+
*
32+
* @return void
33+
*/
34+
public function down()
35+
{
36+
Schema::dropIfExists('visits_archive');
37+
}
38+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Awssat\Visits\Commands;
4+
5+
use Awssat\Visits\DataEngines\RedisEngine;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Redis;
9+
10+
class VisitsArchiveCommand extends Command
11+
{
12+
protected $signature = 'visits:archive';
13+
protected $description = '(Laravel-Visits) Archive daily visits from Redis to the database.';
14+
15+
public function handle()
16+
{
17+
if (! config('visits.archive_daily_visits')) {
18+
$this->error('Daily visits archiving is disabled. Please enable it in config/visits.php');
19+
return;
20+
}
21+
22+
$this->info('Archiving daily visits...');
23+
24+
$redis = app(RedisEngine::class)
25+
->connect(config('visits.connection'))
26+
->setPrefix(config('visits.keys_prefix'));
27+
$prefix = config('visits.keys_prefix');
28+
29+
$keys = $redis->search('*_day_daily_*', false);
30+
31+
foreach ($keys as $key) {
32+
if (\Illuminate\Support\Str::endsWith($key, '_total')) {
33+
continue;
34+
}
35+
36+
$keyWithoutPrefix = substr($key, strlen($prefix) + 1);
37+
38+
$parts = explode('_', $keyWithoutPrefix);
39+
$date = array_pop($parts);
40+
array_pop($parts); // remove daily
41+
array_pop($parts); // remove day
42+
$tag = array_pop($parts); // remove visits
43+
$visitable_type = implode('_', $parts);
44+
45+
if (app()->environment('testing')) {
46+
$visitable_type = str_replace('testing:', '', $visitable_type);
47+
}
48+
49+
$visits = $redis->valueList($keyWithoutPrefix, -1, true, true);
50+
51+
foreach ($visits as $visitable_id => $count) {
52+
DB::table('visits_archive')->insert([
53+
'visitable_type' => $visitable_type,
54+
'visitable_id' => $visitable_id,
55+
'tag' => $tag,
56+
'date' => $date,
57+
'count' => $count,
58+
'created_at' => now(),
59+
'updated_at' => now(),
60+
]);
61+
}
62+
63+
$redis->delete($keyWithoutPrefix);
64+
$redis->delete($keyWithoutPrefix . '_total');
65+
}
66+
67+
$this->info('Done.');
68+
}
69+
}

src/Traits/Record.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ protected function recordPeriods($inc)
4848

4949
$this->connection->increment($periodKey, $inc, $this->keys->id);
5050
$this->connection->increment($periodKey . '_total', $inc);
51+
52+
if ($period === 'day' && config('visits.archive_daily_visits')) {
53+
$this->connection->increment($periodKey . '_daily_' . now()->toDateString(), $inc, $this->keys->id);
54+
$this->connection->increment($periodKey . '_daily_' . now()->toDateString() . '_total', $inc);
55+
}
5156
}
5257
}
5358

src/Traits/Setters.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,15 @@ public function period($period)
9292

9393
return $this;
9494
}
95+
96+
/**
97+
* @param $date
98+
* @return $this
99+
*/
100+
public function byDate($date)
101+
{
102+
$this->date = $date;
103+
104+
return $this;
105+
}
95106
}

src/Visits.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class Visits
4141
* @var null|string
4242
*/
4343
protected $language = null;
44+
/**
45+
* @var null|string
46+
*/
47+
protected $date = null;
4448
/**
4549
* @var mixed
4650
*/
@@ -162,6 +166,15 @@ public function count()
162166
return $this->connection->get($this->keys->visits."_languages:{$this->keys->id}", $this->language);
163167
}
164168

169+
if ($this->date) {
170+
$key = $this->keys->period('day') . '_daily_' . $this->date;
171+
if ($this->keys->instanceOfModel) {
172+
return intval($this->connection->get($key, $this->keys->id));
173+
}
174+
175+
return intval($this->connection->get($key . '_total'));
176+
}
177+
165178
return intval(
166179
$this->keys->instanceOfModel
167180
? $this->connection->get($this->keys->visits, $this->keys->id)

src/VisitsServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ public function getLocation() {
8989
}
9090

9191
$this->app->bind('command.visits:clean', CleanCommand::class);
92+
$this->app->bind('command.visits:archive', \Awssat\Visits\Commands\VisitsArchiveCommand::class);
9293

9394
$this->commands([
9495
'command.visits:clean',
96+
'command.visits:archive',
9597
]);
9698
}
9799
}

src/config/visits.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,16 @@
7272
| stop recording specific items (can be any of these: 'country', 'refer', 'periods', 'operatingSystem', 'language')
7373
|
7474
*/
75-
'global_ignore' => ['country'],
75+
'global_ignore' => [],
7676

77+
/*
78+
|--------------------------------------------------------------------------
79+
| Archive daily visits
80+
|--------------------------------------------------------------------------
81+
|
82+
| If you want to archive daily visits, you need to enable this option.
83+
|
84+
*/
85+
'archive_daily_visits' => true,
7786
];
7887

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Awssat\Visits\Tests\Feature;
4+
5+
use Awssat\Visits\Tests\TestCase;
6+
use Awssat\Visits\Tests\Post;
7+
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Illuminate\Support\Facades\Redis;
9+
use Illuminate\Support\Carbon;
10+
11+
class ArchiveDailyVisitsTest extends TestCase
12+
{
13+
use RefreshDatabase;
14+
15+
public function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->app['config']['database.redis.client'] = 'predis';
20+
$this->app['config']['database.redis.options.prefix'] = '';
21+
$this->app['config']['database.redis.laravel-visits'] = [
22+
'host' => env('REDIS_HOST', 'localhost'),
23+
'password' => env('REDIS_PASSWORD', null),
24+
'port' => env('REDIS_PORT', 6379),
25+
'database' => 3,
26+
];
27+
28+
$this->redis = Redis::connection('laravel-visits');
29+
30+
if (count($keys = $this->redis->keys($this->app['config']['visits.keys_prefix'] . ':testing:*'))) {
31+
$this->redis->del($keys);
32+
}
33+
}
34+
35+
/** @test */
36+
public function it_does_not_record_daily_visits_when_disabled()
37+
{
38+
$this->app['config']['visits.archive_daily_visits'] = false;
39+
40+
$post = Post::create(['id' => 1]);
41+
visits($post)->increment();
42+
43+
$this->assertCount(0, $this->redis->keys('*_day_daily_*'));
44+
}
45+
46+
/** @test */
47+
public function it_throws_an_exception_when_the_archive_command_is_run_while_disabled()
48+
{
49+
$this->app['config']['visits.archive_daily_visits'] = false;
50+
51+
$this->artisan('visits:archive')
52+
->expectsOutput('Daily visits archiving is disabled. Please enable it in config/visits.php')
53+
->assertExitCode(0);
54+
}
55+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Awssat\Visits\Tests\Feature;
4+
5+
use Awssat\Visits\Tests\TestCase;
6+
use Awssat\Visits\Tests\Post;
7+
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Illuminate\Support\Facades\Redis;
9+
use Illuminate\Support\Facades\DB;
10+
use Illuminate\Support\Carbon;
11+
12+
class VisitsArchiveCommandTest extends TestCase
13+
{
14+
use RefreshDatabase;
15+
16+
public function setUp(): void
17+
{
18+
parent::setUp();
19+
20+
$this->app['config']['database.redis.client'] = 'predis';
21+
$this->app['config']['database.redis.options.prefix'] = '';
22+
$this->app['config']['database.redis.laravel-visits'] = [
23+
'host' => env('REDIS_HOST', 'localhost'),
24+
'password' => env('REDIS_PASSWORD', null),
25+
'port' => env('REDIS_PORT', 6379),
26+
'database' => 3,
27+
];
28+
29+
$this->redis = Redis::connection('laravel-visits');
30+
31+
if (count($keys = $this->redis->keys($this->app['config']['visits.keys_prefix'] . ':testing:*'))) {
32+
$this->redis->del($keys);
33+
}
34+
35+
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
36+
}
37+
38+
/** @test */
39+
public function it_archives_daily_visits()
40+
{
41+
Carbon::setTestNow(Carbon::create(2023, 1, 1));
42+
43+
$post = Post::create(['id' => 1]);
44+
visits($post)->increment();
45+
46+
$this->artisan('visits:archive')->assertExitCode(0);
47+
48+
$this->assertDatabaseHas('visits_archive', [
49+
'visitable_type' => 'posts',
50+
'visitable_id' => 1,
51+
'tag' => 'visits',
52+
'date' => '2023-01-01',
53+
'count' => 1,
54+
]);
55+
}
56+
}

0 commit comments

Comments
 (0)