From f7167a85a84c2c39048f525b475cf4cb52771035 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 30 Mar 2021 20:16:15 +0200 Subject: [PATCH] 1.3.1: current visitors --- package.json | 2 +- src/app.js | 10 ++---- src/hit_counter.js | 26 ++++++++++++--- test/app.test.js | 12 +++++-- test/hit_counter.test.js | 70 ++++++++++++++++++++++++++++------------ 5 files changed, 83 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 2f70876..5d63228 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitblog.md", - "version": "1.3.0", + "version": "1.3.1", "description": "A static blog using Markdown pulled from your git repository.", "main": "src/server.js", "dependencies": { diff --git a/src/app.js b/src/app.js index 5dcea61..3364ce2 100644 --- a/src/app.js +++ b/src/app.js @@ -182,10 +182,7 @@ module.exports = (config) => { app.get('/stats', (req, res) => { if (config['modules']['hit_counter']) { hc.read('/', (data) => { - res.json({ - hits: data.hits, - visitors: data.visitors, - }); + res.json(data); }); } else { showError(req, res, 404); @@ -268,10 +265,7 @@ module.exports = (config) => { } else if (req.path.endsWith('stats')) { if (config['modules']['hit_counter']) { hc.read(articlePath, (data) => { - res.json({ - hits: data.hits, - visitors: data.visitors, - }); + res.json(data); }); } else { showError(req, res, 404); diff --git a/src/hit_counter.js b/src/hit_counter.js index e36b22c..a0e25b5 100644 --- a/src/hit_counter.js +++ b/src/hit_counter.js @@ -13,10 +13,10 @@ module.exports = (config, onConnect, onError) => { cb(); } else { const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - const key = path + ':' + ip; + visitors[path] = (visitors[path] || {}); const now = Date.now(); - const isNewVisitor = (now - (visitors[key] || 0)) > config['hit_counter']['unique_visitor_timeout']; - visitors[key] = now; + const isNewVisitor = (now - (visitors[path][ip] || 0)) > config['hit_counter']['unique_visitor_timeout']; + visitors[path][ip] = now; client .multi() .hincrby(path, 'h', 1) @@ -25,17 +25,33 @@ module.exports = (config, onConnect, onError) => { } }; + const cleanVisitors = (path) => { + visitors[path] = (visitors[path] || {}); + const now = Date.now(); + let count = 0; + for (let ip in visitors[path]) { + if ((now - visitors[path][ip]) > config['hit_counter']['unique_visitor_timeout']) { + delete visitors[path][ip]; + } else { + count++; + } + } + return count; + }; + const read = (path, cb) => { if (!client.connected) { cb({ hits: 0, - visitors: 0, + total_visitors: 0, + current_visitors: 0, }); } else { client.hgetall(path, (_, value) => { cb({ hits: value ? value.h || 0 : 0, - visitors: value ? value.v || 0 : 0, + total_visitors: value ? value.v || 0 : 0, + current_visitors: cleanVisitors(path), }); }); } diff --git a/test/app.test.js b/test/app.test.js index b8e0103..ce5fd78 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -209,7 +209,11 @@ describe('Test root path', () => { request(app).get('/stats') .then((response) => { expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ hits: 0, visitors: 0 }); + expect(response.body).toEqual({ + hits: 0, + total_visitors: 0, + current_visitors: 0, + }); done(); }); }); @@ -478,7 +482,11 @@ describe('Test articles rendering', () => { request(app).get('/2019/05/05/anything/stats') .then((response) => { expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ hits: 0, visitors: 0 }); + expect(response.body).toEqual({ + hits: 0, + total_visitors: 0, + current_visitors: 0, + }); done(); }); }); diff --git a/test/hit_counter.test.js b/test/hit_counter.test.js index af5c7c6..c7e3788 100644 --- a/test/hit_counter.test.js +++ b/test/hit_counter.test.js @@ -37,14 +37,21 @@ test('options passed to redis', () => { expect(mockClient.options).toEqual(config['redis']); }); -describe('read()', () => { - beforeEach(() => { - mockClient.hgetall = (_, cb) => { - cb(); - }; - }); - test('read path', (done) => { +beforeEach(() => { + mockClient.hgetall = (_, cb) => { + cb(); + }; + mockClient.multi = () => mockClient; + mockClient.hincrby = () => mockClient; + mockClient.exec = (cb) => { + cb(); + }; + config['hit_counter']['unique_visitor_timeout'] = -1; +}); + +describe('read()', () => { + test('normal', (done) => { mockClient.hgetall = (path, cb) => { expect(path).toBe('/test/path/'); cb(undefined, { h: 12, v: 34 }); @@ -52,12 +59,13 @@ describe('read()', () => { hc.read('/test/path/', (data) => { expect(data).toBeDefined(); expect(data.hits).toBe(12); - expect(data.visitors).toBe(34); + expect(data.total_visitors).toBe(34); + expect(data.current_visitors).toBe(0); done(); }); }); - test('read path with error', (done) => { + test('with error', (done) => { mockClient.hgetall = (path, cb) => { expect(path).toBe('/test/path/'); cb('error', undefined); @@ -65,12 +73,13 @@ describe('read()', () => { hc.read('/test/path/', (data) => { expect(data).toBeDefined(); expect(data.hits).toBe(0); - expect(data.visitors).toBe(0); + expect(data.total_visitors).toBe(0); + expect(data.current_visitors).toBe(0); done(); }); }); - test('read path with error 2', (done) => { + test('with error 2', (done) => { mockClient.hgetall = (path, cb) => { expect(path).toBe('/test/path/'); cb(undefined, {}); @@ -78,22 +87,41 @@ describe('read()', () => { hc.read('/test/path/', (data) => { expect(data).toBeDefined(); expect(data.hits).toBe(0); - expect(data.visitors).toBe(0); + expect(data.total_visitors).toBe(0); + expect(data.current_visitors).toBe(0); done(); }); }); + + test('1 visitor', (done) => { + config['hit_counter']['unique_visitor_timeout'] = 1000; + hc.count({ + headers: {}, + connection: { remoteAddress: 'test1' }, + }, '/test/path/5', () => { + hc.read('/test/path/5', (data) => { + expect(data).toBeDefined(); + expect(data.current_visitors).toBe(1); + done(); + }); + }); + }); + + test('cleaned old visitor', (done) => { + hc.count({ + headers: {}, + connection: { remoteAddress: 'test1' }, + }, '/test/path/5', () => { + hc.read('/test/path/5', (data) => { + expect(data).toBeDefined(); + expect(data.current_visitors).toBe(0); + done(); + }); + }); + }); }); describe('count()', () => { - beforeEach(() => { - mockClient.multi = () => mockClient; - mockClient.hincrby = () => mockClient; - mockClient.exec = (cb) => { - cb(); - }; - config['hit_counter']['unique_visitor_timeout'] = -1; - }); - test('simple visit', (done) => { let multiCalled = false; let execCalled = false;